هياكل البيانات في Python: الفرق بين Stack وQueue
تؤثر بنية البيانات في طريقة الوصول إلى العناصر وفي أداء البرنامج. من أكثر البنى التي تظهر في التطبيقات المكدس Stack والطابور Queue. يخرج من المكدس آخر عنصر دخل إليه، بينما يخرج من الطابور أول عنصر دخل، ولذلك يناسب كل واحد منهما نوعاً مختلفاً من المشكلات. سنبني مثالاً بسيطاً لكل بنية في Python، ثم نربطهما باستخدامات مثل التراجع عن العمليات ومعالجة المهام. فهم الفكرة أهم من حفظ اسم الدالة، لأن اختيار البنية الخاطئة قد يجعل الخوارزمية معقدة أو بطيئة حتى لو كان الكود قصيراً.
الفكرة الأساسية
يعتمد Stack على مبدأ LIFO، أي آخر داخل هو أول خارج. يمكن تمثيل ذلك بمكدس أطباق أو سجل التراجع في محرر نصوص. أما Queue فيعتمد على FIFO، حيث ينتظر العنصر الأقدم دوره أولاً، كما يحدث في طابور المهام. توفر list عمليات مناسبة للمكدس، بينما يناسب collections.deque الطابور لأنه يضيف ويحذف من الطرفين بكفاءة. لا تستخدم list.pop(0) بكثرة للطوابير الكبيرة، لأن تحريك بقية العناصر قد يكلف وقتاً إضافياً.
مثال برمجي عملي
from collections import deque
# مكدس للتراجع عن آخر عمليتين
history = []
history.append("إضافة عنوان")
history.append("تغيير اللون")
history.append("حذف صورة")
last_action = history.pop()
print("التراجع عن:", last_action)
# طابور لمعالجة الطلبات بالترتيب
requests = deque(["job-101", "job-102"])
requests.append("job-103")
while requests:
current = requests.popleft()
print("معالجة", current)
شرح المثال
تستخدم append لإضافة عملية إلى المكدس وpop لسحب آخر عملية. لا يحتاج المثال إلى معرفة عدد العناصر قبل السحب، لكن يجب التأكد من أن القائمة ليست فارغة. في الطابور نستخدم deque، وتضيف append المهمة إلى النهاية، بينما popleft يسحب أقدم مهمة من البداية. حلقة while تستمر حتى يخلو الطابور. في نظام حقيقي قد تحتاج إلى انتظار مهام جديدة بدلاً من إنهاء الحلقة، وربما إلى أكثر من عامل يعالج العناصر مع حماية من الوصول المتزامن.
قبل اختيار البنية، اكتب جملة تصف ترتيب الإخراج المطلوب. إذا قلت آخر تعديل أولاً فالمكدس مناسب، وإذا قلت الأقدم أولاً فالطابور أقرب إلى المطلوب. افصل عمليات الإضافة والإزالة في دوال صغيرة حتى لا تنتشر تفاصيل البنية في كل البرنامج. أضف اختباراً للحالة الفارغة، ولعنصر واحد، ولعدة عناصر. بهذه الطريقة يظهر الخطأ في ترتيب العناصر بسرعة ولا تختلط المشكلة بمنطق التطبيق.
طريقة العمل قبل كتابة الكود
قبل فتح محرر النصوص، اكتب النتيجة التي تريد الوصول إليها وحدد المدخلات والمخرجات. يساعد هذا التمرين على كشف الحالات الغامضة مبكراً، مثل قيمة ناقصة أو قائمة فارغة أو طلب لا يصل إلى الخادم. لا تحتاج إلى مخطط كبير للمثال التعليمي، لكنك تحتاج إلى أسماء واضحة وخطوات يمكن اختبارها واحدة بعد أخرى. عندما تتغير الفكرة أثناء الكتابة، عدل التصميم قبل إضافة شروط متفرعة يصعب تتبعها.
قسّم المشكلة إلى أجزاء صغيرة، واجعل كل جزء مسؤولاً عن قرار واحد قدر الإمكان. يمكن أن تكون الأجزاء دوال، أو مكونات، أو طبقات منفصلة بحسب التقنية. لا يعني التقسيم إنشاء ملفات كثيرة، بل يعني أن تعرف أين تبحث عندما يحدث الخطأ. سجّل الافتراضات المهمة بجملة قصيرة، مثل أن القائمة مرتبة أو أن السعر غير سالب، ثم تحقق منها في المكان المناسب بدلاً من الاعتماد على الذاكرة.
اختبار المثال في حالات مختلفة
لا تختبر المسار الطبيعي فقط. جرّب مدخلاً فارغاً، وقيمة أكبر من المتوقع، وعنصراً غير موجود، وطلباً يصل من دون البيانات المطلوبة. الحالات الحدية تكشف غالباً مشكلات الفهارس والأنواع وترتيب التنفيذ. اكتب النتيجة المتوقعة قبل تشغيل الكود، ثم قارنها بالنتيجة الفعلية. إذا كان الاختبار يمر بالصدفة، أضف شرطاً أو اختباراً أوضح، ولا تكتفِ بطباعة قيمة تبدو منطقية.
- تحقق من المدخلات قبل استخدامها في الحساب أو التخزين.
- أعد رسالة مفهومة ورمزاً مناسباً عند وقوع الخطأ.
- اجعل الاختبارات قابلة للإعادة من دون اعتماد على شبكة أو وقت متغير.
- راجع أثر التغيير على الأجزاء التي تستدعي الدالة أو المكون.
ملاحظات تتعلق بجودة الكود
تظهر جودة الحل في التفاصيل الصغيرة: اسم يشرح الغرض، ودالة لا تجمع مهاماً بعيدة، ورسالة خطأ لا تترك القارئ في حيرة. لا تحاول اختصار كل سطر، فالكود المقروء أفضل من تعبير قصير يحتاج إلى شرح طويل. وفي الوقت نفسه، لا تكرر القاعدة نفسها في أماكن كثيرة؛ انقلها إلى موضع واحد عندما يكون ذلك أوضح. راجع الملف بعد أن يعمل، لأن أول نسخة تركز عادةً على الوصول إلى النتيجة أكثر من قابلية الصيانة.
احتفظ بالإعدادات التي تختلف بين جهاز وآخر خارج الكود، ولا تضع كلمات مرور أو مفاتيح خاصة في المستودع. استخدم سجلات مناسبة أثناء التطوير، ثم راجع ما ينبغي حجبه في بيئة التشغيل. إذا تعامل البرنامج مع بيانات المستخدم، فافصل بين ما يحتاجه التطبيق وما يمكن الاحتفاظ به. هذه الممارسات لا تخص لغة واحدة، بل تقلل المشكلات عندما يكبر المشروع أو يعمل عليه أكثر من شخص.
متى تعرف أن الحل يحتاج إلى تطوير؟
يحتاج المثال التعليمي إلى طبقات إضافية عندما يدخل في نظام حقيقي: قاعدة بيانات، مستخدمون متعددون، مراقبة، اختبارات، وصلاحيات. لا تضف هذه الأجزاء قبل معرفة المشكلة التي تحلها، لكن لا تنقل الكود التجريبي إلى الإنتاج كما هو. راقب حجم البيانات، وعدد الطلبات، ومصدر المدخلات، وما إذا كان الفشل يجب أن يعيد العملية أو يوقفها. اكتب قرارك في مستند صغير أو تعليق يشرح السبب، حتى لا يضطر الفريق إلى تخمينه لاحقاً.
إذا وجدت أن الخطأ يتكرر في أكثر من مكان، فابحث عن قاعدة مشتركة. وإذا أصبح التعديل في ملف صغير يؤثر في ملفات كثيرة، فراجع حدود المسؤوليات. لا توجد بنية واحدة صحيحة لكل مشروع، لكن توجد أسئلة تساعدك على اختيار بنية مناسبة: من يملك البيانات؟ من يغيرها؟ ماذا يحدث عند الفشل؟ وكيف يمكن اختبار الجزء من دون تشغيل النظام كله؟
أخطاء ينبغي تجنبها
قد يخلط المطور بين pop وpopleft، أو يستخدم قائمة عادية لطابور ضخم من دون قياس. من الأخطاء أيضاً التعامل مع المكدس على أنه تخزين دائم، مع أن البيانات قد تضيع عند انتهاء العملية ما لم تحفظها. لا تجعل بنية البيانات مسؤولة عن الاتصال بقاعدة البيانات أو عرض الرسائل. اجعلها صغيرة ومحددة، ثم ضع سياسة إعادة المحاولة أو الحذف في الطبقة التي تعرف معنى المهمة.
خطوة تالية مناسبة
جرّب بناء تاريخ تراجع لعمليات نصية، ثم طبّق طابوراً يحوي أولوية باستخدام heapq عندما لا يكون ترتيب الوصول كافياً. قارن الذاكرة والزمن على بيانات صغيرة وكبيرة. عندما تفهم سلوك كل بنية، ستجدها في خوارزميات البحث، وجدولة المهام، وتحليل التعبيرات، وهي استخدامات عملية تتجاوز الأمثلة التعليمية.
اختبار بنية البيانات قبل استخدامها
اكتب حالات بسيطة لكل عملية قبل وضع Stack أو Queue داخل مشروع حقيقي. في المكدس، أضف ثلاثة عناصر ثم أزلها وتأكد من عكس الترتيب. وفي الطابور، أضف العناصر نفسها وتأكد من خروجها بالترتيب الذي دخلت به. جرّب أيضاً البنية وهي فارغة، ولاحظ هل تعيد None أم ترمي استثناءً. هذا القرار يجب أن يكون موثقاً، لأن بقية البرنامج سيبني سلوكه عليه. عندما يصبح عدد العناصر كبيراً، راقب استهلاك الذاكرة وطريقة حذف العنصر الأول؛ فاختيار قائمة أو deque في Python قد يغير الأداء كثيراً من دون أن تتغير فكرة الخوارزمية نفسها.
اختيار البنية حسب العملية
لا توجد بنية بيانات أفضل في كل الحالات. إذا كان المطلوب الوصول إلى عنصر بمفتاح، فقد يكون القاموس أو مجموعة مناسبة أكثر. وإذا كان المطلوب معالجة العناصر بالترتيب، فالسؤال هو هل تحتاج إلى البداية أم النهاية. اكتب العملية الأكثر تكراراً قبل الاختيار، ثم قارِن كلفتها بدلاً من الاعتماد على الاسم الشائع للبنية.
الخلاصة
جرّب بناء تاريخ تراجع لعمليات نصية، ثم طبّق طابوراً يحوي أولوية باستخدام heapq عندما لا يكون ترتيب الوصول كافياً. قارن الذاكرة والزمن على بيانات صغيرة وكبيرة. عندما تفهم سلوك كل بنية، ستجدها في خوارزميات البحث، وجدولة المهام، وتحليل التعبيرات، وهي استخدامات عملية تتجاوز الأمثلة التعليمية. يوضح هذا الموضوع كيف يتحول مفهوم نظري إلى خطوات يمكن تشغيلها وفحصها.
ابدأ بتطبيق المثال على ملف صغير، ثم غيّر مدخلاً واحداً وراقب النتيجة. بعد ذلك أضف حالة فشل واكتب اختباراً لها، ثم انقل الفكرة إلى مشروعك الحقيقي بحذر. عندما تفهم سبب كل خطوة، ستستطيع تغيير الأدوات أو اللغة من دون فقدان المفهوم. البرمجة تتحسن بالمحاولات القصيرة والمراجعة المستمرة، لا بنسخ كود طويل من دون معرفة ما الذي يحميه أو ما الذي قد يكسره.