بعد أن تعرفت في الدروس السابقة على البنى الأساسية والقوائم (Lists) في بايثون، ستلاحظ مع تقدمك في بناء تطبيقات الويب والأنظمة الخلفية (Backend) أن القوائم ليست دائمًا الخيار الأمثل لكل سيناريو برمجي. تخزين البيانات يتطلب في كثير من الأحيان قواعد صارمة: إما لحماية البيانات من التعديل العرضي أثناء انتقالها بين الطبقات المختلفة، أو لضمان عدم تكرار القيم، أو لإجراء عمليات بحث واختبار وجود (Membership Testing) سريعة للغاية في الذاكرة.
في هذا الدليل العملي، سنشرح بالتفصيل بنيتين من أهم هياكل البيانات في بايثون: الصفوف (Tuples) والمجموعات (Sets). سنركز على الجوانب التطبيقية، الفروق الجوهرية في الأداء وإدارة الذاكرة، وكيفية توظيفهما بكفاءة في مهام تطوير الويب مثل إدارة الصلاحيات ومعالجة سجلات قواعد البيانات وإعدادات النظام.
أولاً: الصفوف (Tuples) - البيانات غير القابلة للتعديل
الصف (Tuple) هو تسلسل مرتب من العناصر يشبه القائمة، لكنه يتميز بخاصية جوهرية تجعله مختلفًا تمامًا: عدم قابلية التعديل (Immutability). بمجرد إنشاء الـ Tuple في الذاكرة، لا يمكنك إضافة عناصر جديدة، أو حذف عناصر، أو تعديل قيم العناصر الموجودة داخله.
1. إنشاء الصفوف وحالة العنصر الواحد
تُعرّف الصفوف عادةً باستخدام الأقواس الدائرية () مع فصل العناصر بفواصل. هناك تفصيلة دقيقة يقع فيها الكثير من المطورين المبتدئين عند إنشاء صف يحتوي على عنصر واحد فقط:
# إنشاء صف يحتوي على عدة عناصر
server_config = ("192.168.1.1", 8080, "production")
# محاولة خاطئة لإنشاء صف بعنصر واحد (سيعتبره بايثون نصًا String بين أقواس)
not_a_tuple = ("admin")
print(type(not_a_tuple)) # <class 'str'>
# الطريقة الصحيحة: إضافة فاصلة بعد العنصر
single_item_tuple = ("admin",)
print(type(single_item_tuple)) # <class 'tuple'>الفاصلة هي التي تحدد نوع الـ Tuple بالنسبة لمفسر بايثون، وليس الأقواس وحدها. الأقواس تساعد فقط في توضيح الحدود البنائية للكود وتجنب الغموض عند استدعاء الدوال.
2. فك الحزم (Tuple Unpacking) والفك الممتد
تُعد ميزة فك الحزم من أكثر التقنيات استخدامًا في بايثون، لا سيما عند استقبال بيانات من استعلامات قواعد البيانات (SQL Queries) أو عند التعامل مع الدوال التي تعيد قيمًا متعددة:
# بيانات مسترجعة من قاعدة البيانات لمنتج في متجر
product_record = (104, "Mechanical Keyboard", 89.99, True)
# فك الحزم المباشر
product_id, name, price, is_available = product_record
print(name) # Mechanical Keyboard
print(price) # 89.99
# استخدام عامل النجمة (*) لالتقاط باقي العناصر (Extended Unpacking)
route_data = ("GET", "/api/v1/users", 200, "application/json", "gzip")
method, endpoint, status_code, *headers = route_data
print(method) # GET
print(headers) # ['application/json', 'gzip']في المثال السابق، التقط المتغير *headers جميع العناصر المتبقية داخل قائمة، وهو أسلوب شائع جدًا عند معالجة طلبات الـ HTTP ومعاملات التوجيه (Routing Frameworks) في إطارات العمل مثل Django وFastAPI.
3. الدوال المتاحة مع الصفوف (Tuple Methods)
نظراً لأن الصفوف غير قابلة للتعديل، فإن الدوال الخاصة بها محصورة في عمليات القراءة والاستعلام فقط، مما يجعلها خفيفة وسريعة:
status_codes = (200, 404, 500, 200, 403, 200)
# حساب عدد مرات تكرار عنصر معين
success_count = status_codes.count(200)
print(success_count) # 3
# البحث عن فهرس (Index) أول ظهور لعنصر
first_error = status_codes.index(404)
print(first_error) # 14. لماذا نستخدم Tuples بدلاً من Lists؟
- حماية تكامل البيانات (Data Integrity): عند تمرير إعدادات اتصال بقاعدة بيانات أو ثوابت عبر طبقات التطبيق، يضمن الـ Tuple عدم قيام أي دالة بتغيير تلك القيم بالخطأ أثناء التنفيذ.
- استخدامها كمفاتيح للقواميس (Dictionary Keys): لأن الـ Tuples غير قابلة للتعديل، فهي تمتلك قيمة تجزئة ثابتة (Hashable)، مما يسمح باستخدامها كمفاتيح داخل القواميس، مثل تخزين إحداثيات جغرافية أو مفاتيح ذاكرة التخزين المؤقت (Cache Keys):
cache[(lat, lon)] = "Server Alpha"، وهو أمر مستحيل مع القوائم. - كفاءة الذاكرة والأداء: تتطلب الصفوف تخصيص ذاكرة دقيق ومحكم من مفسر بايثون؛ نظرًا لعدم الحاجة إلى إبقاء مساحة إضافية للتوسع المستقبلي كما هو الحال في القوائم الديناميكية. هذا يقلل من حجم الكائنات داخل الذاكرة ويزيد من سرعة إنشائها ومعالجتها.
ثانياً: المجموعات (Sets) - التفرد والعمليات الرياضية
المجموعة (Set) هي بنية بيانات غير مرتبة (Unordered) تحتوي فقط على عناصر فريدة لا تقبل التكرار. تعتمد المجموعات في تخزين عناصرها على جدول التجزئة (Hash Table)، مما يجعل عملية البحث والتحقق من وجود عنصر داخل المجموعة تتم بسرعة هائلة بتعقيد زمني ثابت تقريبًا $O(1)$، بغض النظر عن حجم المجموعة.
1. إنشاء المجموعات والفرق بين {} وset()
تُنشأ المجموعات باستخدام الأقواس المعقوفة {} أو الدالة البنائية set(). يجب الانتباه إلى أن إنشاء مجموعة فارغة يتطلب استخدام set() حصريًا، لأن الأقواس الفارغة {} تُنشئ قاموسًا فارغًا (Dictionary):
# إزالة التكرار تلقائيًا من قائمة وسوم المقالات
raw_tags = ["python", "webdev", "backend", "python", "api", "webdev"]
unique_tags = set(raw_tags)
print(unique_tags) # {'python', 'webdev', 'backend', 'api'}
# إنشاء مجموعة فارغة بالطريقة الصحيحة
empty_set = set()
print(type(empty_set)) # <class 'set'>
# محاولة خاطئة
not_a_set = {}
print(type(not_a_set)) # <class 'dict'>2. تعديل المجموعات: الإضافة والحذف
المجموعات قابلة للتعديل (Mutable)، مما يعني أنه يمكنك إضافة عناصر جديدة أو حذف عناصر حالية أثناء تشغيل التطبيق:
active_sessions = {"session_123", "session_456"}
# إضافة عنصر واحد
active_sessions.add("session_789")
# إضافة عناصر متعددة من هيكل بيانات آخر
active_sessions.update(["session_101", "session_102"])
# الحذف باستخدام remove (تطلق خطأ KeyError إذا لم يكن العنصر موجودًا)
active_sessions.remove("session_123")
# الحذف الآمن باستخدام discard (لا تطلق خطأ إذا لم يكن العنصر موجودًا)
active_sessions.discard("non_existing_session")
# سحب وإعادة عنصر عشوائي من المجموعة
removed_session = active_sessions.pop()تُعتبر دالة discard() خيارًا ممتازهًا في تطبيقات الويب عند مسح الجلسات أو الرموز المميزة (Tokens) لتجنب توقف التطبيق في حال حاول المستخدم تسجيل الخروج مرتين متتاليتين.
3. العمليات الجبرية على المجموعات (Set Operations)
توفر المجموعات أدوات رياضية ممتازة لإجراء المقارنات والتقاطعات بين البيانات بدون الحاجة إلى كتابة حلقات تكرارية معقدة وطويلة:
# صلاحيات الأدوار في نظام إدارة المحتوى
admin_permissions = {"read", "write", "delete", "manage_users"}
editor_permissions = {"read", "write", "publish"}
# 1. الاتحاد (Union): كل الصلاحيات المتاحة بدون تكرار
all_permissions = admin_permissions | editor_permissions
# أو باستخدام الدالة: admin_permissions.union(editor_permissions)
# 2. التقاطع (Intersection): الصلاحيات المشتركة فقط
shared_permissions = admin_permissions & editor_permissions
print(shared_permissions) # {'read', 'write'}
# 3. الفرق (Difference): الصلاحيات الخاصة بالمدير فقط
admin_only = admin_permissions - editor_permissions
print(admin_only) # {'delete', 'manage_users'}
# 4. الفرق المتماثل (Symmetric Difference): صلاحيات يملكها أحدهما دون الآخر
exclusive_perms = admin_permissions ^ editor_permissions
print(exclusive_perms) # {'delete', 'manage_users', 'publish'}هذه العمليات تنفذ مباشرة على مستوى لغة C المكتوب بها مفسر بايثون (CPython)، مما يمنحها أداءً أسرع بمئات المرات مقارنة بتمثيلها عبر دوال التكرار التقليدية for loops في القوائم.
4. المجموعات الثابتة (frozenset)
إذا كنت بحاجة إلى الاستفادة من خصائص المجموعات (عدم التكرار والعمليات الرياضية) وفي الوقت نفسه تحتاج إلى جعل هذه المجموعة غير قابلة للتعديل (Immutable) لاستخدامها كمفتاح في قاموس أو وضعها داخل مجموعة أخرى، توفر بايثون النوع frozenset:
# إنشاء مجموعة ثابتة غير قابلة للتعديل
immutable_permissions = frozenset(["read", "write"])
# استخدامها كمفتاح في قاموس الصلاحيات
role_mapping = {
immutable_permissions: "Standard Editor Role"
}
print(role_mapping[immutable_permissions]) # Standard Editor Roleتطبيق عملي: نظام التحقق من الصلاحيات (RBAC Middleware)
لنربط بين الصفوف والمجموعات في سيناريو برمجي عملي يعكس كيفية بناء وسيط برمجي (Middleware) لفحص صلاحيات المستخدم في تطبيق ويب قبل السماح له بالوصول إلى مسارات محمية (Protected Endpoints):
def check_access(user_roles, required_permissions, role_permissions_map):
"""
التحقق مما إذا كان المستخدم يمتلك الصلاحيات الكافية للوصول.
"""
# تجميع كل صلاحيات المستخدم في مجموعة واحدة فريدة
user_actual_permissions = set()
for role in user_roles:
if role in role_permissions_map:
user_actual_permissions.update(role_permissions_map[role])
# التحقق من أن مجموعة الصلاحيات المطلوبة هي مجموعة جزئية (Subset)
has_permission = required_permissions.issubset(user_actual_permissions)
return has_permission
# تعريف خريطة الصلاحيات كصفوف ثابتة لحمايتها من التعديل أثناء التشغيل
ROLE_MAP = {
"guest": ("read_public",),
"author": ("read_public", "write_post", "edit_own_post"),
"moderator": ("read_public", "edit_own_post", "delete_post", "ban_user")
}
# المستخدم الحالي يمتلك دورين (محفوظة كـ Tuple)
current_user_roles = ("guest", "author")
# الصلاحيات المطلوبة للوصول لنقطة النهاية الخاصة بتحرير المقال
route_required_perms = {"read_public", "write_post"}
# تنفيذ فحص الصلاحية
access_granted = check_access(current_user_roles, route_required_perms, ROLE_MAP)
print(f"Access Status: {'Granted' if access_granted else 'Denied'}")
# النتيجة: Access Status: Grantedشرح وتفكيك التطبيق العملي:
- استخدمنا الـ Tuples في
ROLE_MAPلضمان عدم قيام أي ثغرة أو كود جانبي بتعديل الصلاحيات المعرفة للأدوار في الذاكرة أثناء معالجة الطلبات. - استخدمنا الـ Sets لدمج وتجميع الصلاحيات المتعددة بدون تكرار، واستعنا بالدالة
issubset()للتحقق المباشر والسريع من توافر كافة الصلاحيات المطلوبة للوصول.
أخطاء شائعة وقيود برمجية يجب تجنبها
1. فخ ثبات العناصر المتغيرة داخل الصفوف (Shallow Immutability)
خاصية ثبات الـ Tuple تعني أن المراجع (References) المخزنة بداخله لا يمكن تغييرها لتمشير إلى كائنات أخرى، لكن إذا كان الصف يحتوي على كائن متغير بطبيعته مثل قائمة (List)، فإن محتوى تلك القائمة يمكن تعديله بدون أخطاء:
# صف يحتوي على اسم البيئة وقائمة بالمنافذ المفتوحة
config_data = ("API_V1", [80, 443])
# محاولة تغيير العنصر الأول ستفشل بـ TypeError
# config_data[0] = "API_V2"
# لكن تعديل القائمة الداخلية سينجح!
config_data[1].append(8080)
print(config_data) # ('API_V1', [80, 443, 8080])لتفادي هذا السلوك غير المتوقع، احرص دائمًا على أن تكون جميع العناصر المضمنة داخل الـ Tuple من أنواع غير قابلة للتعديل أيضًا (مثل النصوص والأرقام والصفوف الأخرى).
2. محاولة الوصول لعناصر المجموعة عبر الفهرس (Index)
نظرًا لأن المجموعات غير مرتبة وتعتمد على التجزئة (Hash)، فهي لا تدعم مفهوم الفهرسة (Indexing) أو التقطيع (Slicing):
tags = {"web", "python", "css"}
# محاولة خاطئة ستؤدي إلى TypeError: 'set' object is not subscriptable
# first_tag = tags[0]
# الطريقة الصحيحة: التكرار أو التحقق المباشر من الوجود
if "python" in tags:
print("Tag Python exists!")3. محاولة إضافة كائنات غير قابلة للتجزئة (Unhashable) إلى المجموعة
لا يمكنك إضافة القوائم أو القواميس أو المجموعات العادية مباشرة داخل Set، لأن المجموعات تتطلب أن تكون كافة عناصرها قابلة للتجزئة ولها قيمة Hash ثابتة:
# محاولة خاطئة ستسبب TypeError: unhashable type: 'list'
# invalid_set = {[1, 2], [3, 4]}
# الحل الصحيح: استخدام frozenset أو Tuples
valid_set = {frozenset([1, 2]), frozenset([3, 4])}
valid_tuple_set = {(1, 2), (3, 4)}مقارنة شاملة لتحديد الهيكل المناسب
لضمان اختيار بنية البيانات الأكثر ملاءمة لمهامك البرمجية، يوضح الجدول التالي المقارنة الفنية بين القوائم، الصفوف، والمجموعات:
| الميزة / الخاصية | القائمة (List) | الصف (Tuple) | المجموعة (Set) |
|---|---|---|---|
| الترتيب (Ordered) | نعم (محافظة على الترتيب) | نعم (محافظة على الترتيب) | لا (غير مرتبة) |
| قابليتها للتعديل (Mutability) | نعم (Mutable) | لا (Immutable) | نعم (Mutable - إضافة وحذف) |
| تكرار العناصر (Duplicates) | مسموح | مسموح | ممنوع (تزال تلقائياً) |
| سرعة البحث ($x \in S$) | بطيئة $O(n)$ | بطيئة $O(n)$ | سريعة جداً $O(1)$ |
| استخدامها كمفاتيح قواميس | غير مسموح | مسموح (إذا كانت عناصرها ثابته) | غير مسموح (يستخدم frozenset) |
| الاستخدام المثالي في الويب | مجموعات بيانات متغيرة ومسلسلة | الإعدادات، سجلات DB، القيم الثابتة | تصفية التكرارات، فحص الصلاحيات والروابط |
خلاصة الدرس
اختيار بنية البيانات المناسبة ليس مجرد تفصيل شكلي أو تفضيل شخصي في أسلوب البرمجة، بل هو قرار هندسي أسياسي يؤثر بشكل مباشر على أمان الكود وسرعة استجابة التطبيق واستهلاك الذاكرة. استخدم الصفوف (Tuples) عندما تتعامل مع بيانات مترابطة وثابتة تشكل سجلاً واحدًا غير قابل للتعديل لتضمن حماية البيانات من الأخطاء أثناء التنفيذ. واستخدم المجموعات (Sets) عندما تحتاج إلى تصفية البيانات المكررة بسرعة أو إجراء عمليات تقاطع ومطابقة عالية الأداء بين المجموعات المختلفه.
في الدرس القادم، سنتعمق في القواميس المتقدمة (Dictionaries & Dict Comprehensions) ونستعرض كيف تتكامل هذه الهياكل معًا لبناء نماذج معالجة بيانات احترافية ومتكاملة داخل تطبيقات بايثون.
