الصفوف (tuples) وفك الحزم (unpacking)
قائمة غير قابلة للتعديل — متى تحتاج إليها، واستخدام فك الحزم للتعامل مع الأسماء بدلاً من الفهارس، والتبديل في سطر واحد، وفخ الفرق بين (5) و (5,).
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
نريد تسجيل بيانات الطقس لثلاثة أيام — لكل يوم: اسم اليوم، ودرجة الحرارة، والرطوبة.
يمكن فعل ذلك بقائمة (list): ["monday", 31.5, 78]. سيعمل هذا، لكن هناك أمرين مزعجين.
أولاً، هذه القائمة قابلة للتعديل. إذا نفّذ أحد append على سجل يوم الاثنين بالخطأ، فلن تقول بايثون شيئاً — مع أن السجل لا يُفترض أن يحتوي على أربعة أشياء أصلاً. وضع شيء لا يُفترض أن يتغير في وعاء يمكن تغييره خطر صامت.
ثانياً، لإخراج القيم الثلاث من الداخل تكتب record[0] وrecord[1] وrecord[2] — بالأرقام، لا بالأسماء. بعد ستة أشهر، من يقرأ record[1] لن يعرف هل هي درجة الحرارة أم الرطوبة.
الصف (tuple) هو الجواب عن الأمرين: إنه مثل القائمة لكنه غير قابل للتعديل — ومعه ميزة اسمها فك الحزم (unpacking)، تسمح لك باستخدام الأسماء بدل الفهارس.
في نهاية هذا الفصل ستكون قادراً على
- إنشاء صف، ومعرفة أين تحتاج إلى صف لا إلى قائمة
- وضع عدة قيم في عدة أسماء دفعة واحدة باستخدام فك الحزم
- تبديل قيمتي متغيرين في سطر واحد
- ملاحظة الفرق بين
(5)و(5,) - شرح سبب ظهور
AttributeErrorعند استدعاءappendعلى صف
المتطلبات السابقة: ترتيب القوائم ودوالها العضوة.
الصف — قائمة لا يمكن تعديلها
أقواس عادية (دائرية) بدل الأقواس المربعة:
point = (3, 7)
print(point)
print(point[0])
print(len(point))(3, 7)
3
2القراءة، والفهرس (index)، وlen، والشريحة، وin، والمرور بـ for — كل ما كنت تفعله مع القائمة يعمل هنا. الفرق واحد فقط، وهو كل شيء:
point = (3, 7)
point[0] = 5TypeError: 'tuple' object does not support item assignmentهل تبدو الرسالة مألوفة؟ إنها الرسالة نفسها التي ظهرت في الفصل 6 عند محاولة تغيير حرف داخل سلسلة نصية (string)، مع 'str' بدل 'tuple'. الأشياء غير القابلة للتعديل في بايثون تشتكي باللغة نفسها.
ولا توجد أيضاً الدوال العضوة (methods) التي تعدّل:
point = (3, 7)
point.append(9)AttributeError: 'tuple' object has no attribute 'append'يعني AttributeError: “هذا الكائن (object) لا يحتوي على شيء بهذا الاسم”. الصف لا يمكن تعديله، لذلك لا توجد في الصف دالة عضو اسمها append أصلاً. تتوقف بايثون أثناء البحث عن point.append، قبل أن تحاول إضافة 9. قارن ذلك بالمثال السابق: طلب point[0] = 5 عملية لا يسمح بها الصف، فكان الخطأ TypeError؛ أما هنا فالدالة العضو نفسها غير موجودة، فالخطأ AttributeError.
لماذا هذه ميزة وليست عائقاً
يبدو الصف في البداية قائمة بقدرات أقل. فكّر بالعكس: إذا وضعت ما لا يُفترض أن يتغير في وعاء لا يمكن تغييره، يتوقف الخطأ قبل أن يحدث.
لا معنى لإضافة رقم ثالث إلى إحداثيات نقطة (3, 7). ولا يُفترض أن يحتوي تاريخ (2026, 9, 16) على جزء رابع. عندما تضع هذه الأشياء في صفوف، يُكتشف الحادث فوراً على شكل خطأ، لا لاحقاً على شكل خلل خفي.
كقاعدة: عدد العناصر ومعانيها معروفة مسبقاً — صف. العدد غير معروف، وكل العناصر من النوع نفسه — قائمة.
لذلك بيانات الطقس لثلاثة أيام هي صفوف داخل قائمة: عدد الأيام غير معروف (قائمة)، لكن كل سجل فيه ثلاثة أشياء بالضبط (صف).
فك الحزم — هنا الفائدة الحقيقية
point = (3, 7)
x, y = point
print(x)
print(y)3
7اسمان وقيمتان في سطر واحد. والآن لم يعد point[1]، بل y — اسم تفهمه بمجرد قراءته.
يجب أن يكون عدد الأسماء في اليسار مساوياً تماماً لعدد القيم في اليمين:
point = (3, 7)
x, y, z = pointValueError: not enough values to unpack (expected 3, got 2)هذه الرسالة مفيدة على غير العادة — تخبرك بالعدد المتوقع وبالعدد الذي وصل.
فك الحزم لا يقتصر على الصفوف، بل يعمل مع القوائم أيضاً. لكنه أكثر فائدة مع الصفوف، لأن عدد العناصر في الصف معروف مسبقاً.
التبديل — في سطر واحد
في لغات أخرى يحتاج تبديل قيمتي متغيرين إلى متغير ثالث مؤقت. في بايثون لا حاجة إليه:
a = 1
b = 2
a, b = b, a
print(a, b)2 1من المهم فهم كيف يعمل هذا السطر: تحسب بايثون الطرف الأيمن كاملاً أولاً، فتنشئ صفاً (2, 1)، ثم تفك حزمه في الطرف الأيسر. لذلك حتى عندما يتغير a تكون القيمة القديمة لـ b محفوظة مسبقاً، ولا يضيع شيء.
الأقواس ليست ضرورية في الحقيقة
point = 3, 7
print(point)
print(type(point))(3, 7)
<class 'tuple'>الذي ينشئ الصف هو الفاصلة، لا الأقواس. الأقواس لتسهيل القراءة فقط. إذا عرفت هذا فلن يبدو السطر a, b = b, a سحراً — فالطرف الأيمن b, a صف.
ومن هنا يأتي فخ:
one = (5)
really = (5,)
print(type(one))
print(type(really))
print(len(really))<class 'int'>
<class 'tuple'>
1(5) ليس صفاً — إنه مجرد رقم داخل أقواس، تماماً مثل (2 + 3). لإنشاء صف بقيمة واحدة لا بد من الفاصلة: (5,). تبدو كخطأ مطبعي، لكن تلك الفاصلة هي الفرق كله.
الصفوف في الحلقات
people = [("rafi", 22), ("ahmed", 35)]
for name, age in people:
print(f"{name} is {age}")rafi is 22
ahmed is 35تفك for name, age in people حزمة كل صف مباشرة إلى اسمين. لا حاجة إلى كتابة person[0] وperson[1] — وتفهم من قراءة الكود أي قيمة هي أي شيء.
لقد استخدمت هذه الطريقة من قبل دون أن تعرف:
stock = ["pen", "bag"]
for pair in enumerate(stock):
print(pair)(0, 'pen')
(1, 'bag')تعطي enumerate صفوفاً في الحقيقة. عندما كتبنا for position, item in enumerate(...) في الفصل 12 كنا نفك حزم تلك الصفوف — لكننا لم نكن نعرف الاسم بعد.
التحويل بين النوعين
as_list = [3, 7]
as_tuple = (3, 7)
as_list.append(9)
print(as_list)
print(as_tuple)
print(list(as_tuple))
print(tuple(as_list))[3, 7, 9]
(3, 7)
[3, 7]
(3, 7, 9)تنشئ list() وtuple() أحدهما من الآخر — تنشئان شيئاً جديداً، ويبقى القديم كما هو. إذا أردت “تعديل” صف فهذه هي الطريقة: أنشئ قائمة، عدّلها، ثم أنشئ صفاً من جديد.
مثال كامل
weather.py:
# A tuple is the right shape for a record that must not change
readings = [
("monday", 31.5, 78),
("tuesday", 33.0, 71),
("wednesday", 29.5, 85),
]
print("Day Temp Humidity")
for day, temp, humidity in readings:
print(f"{day:<10} {temp:>5} {humidity:>5}")
hottest = readings[0]
for reading in readings:
if reading[1] > hottest[1]:
hottest = reading
day, temp, humidity = hottest
print()
print(f"Hottest was {day} at {temp}")
temps = []
for _, temp, _ in readings:
temps.append(temp)
print("Temps :", temps)
print("Average :", round(sum(temps) / len(temps), 2))Day Temp Humidity
monday 31.5 78
tuesday 33.0 71
wednesday 29.5 85
Hottest was tuesday at 33.0
Temps : [31.5, 33.0, 29.5]
Average : 31.33أربعة أمور تستحق الانتباه.
قائمة في الخارج، وصفوف في الداخل. عدد الأيام قد يزيد، لذلك الخارجية قائمة. كل سجل فيه ثلاثة أشياء بالضبط، لذلك الداخلية صفوف. هذا هو الشكل الأكثر شيوعاً للبيانات الحقيقية.
f"{day:<10}" وf"{temp:>5}" — للحفاظ على استقامة الأعمدة. < تحاذي إلى اليسار، و> إلى اليمين، والرقم يحدد المساحة التي تشغلها القيمة. مثل :.2f في الفصل 6، ما بعد النقطتين تنسيق فقط.
احتجنا إلى كتابة reading[1] للعثور على اليوم الأشد حرارة، لأن الصف كله بين أيدينا وقت المقارنة، وليس مفكوك الحزم. وبعد العثور عليه فُكت حزمته فوراً — لذلك لا يوجد فهرس في السطر الذي يليه.
for _, temp, _ in readings — الشرطة السفلية عرف شائع معناه “لا أحتاج إلى هذه القيمة”. بالنسبة لبايثون هي اسم مثل أي اسم آخر، لكنها عند القراءة توضح أي جزء يُترك عمداً.
حالات الخطأ الشائعة وحلولها
TypeError: 'tuple' object does not support item assignment محاولة تغيير قيمة داخل صف. الصف لا يمكن تعديله — إما أن تستخدم قائمة من البداية، أو تحوّله بـ list(...) وتعمل عليه.
AttributeError: 'tuple' object has no attribute 'append' السبب نفسه بشكل آخر. append وremove وsort — لا توجد في الصف أي دالة عضو تعدّل. لكن sorted(my_tuple) تعمل، لأنها تعيد قائمة جديدة ولا تلمس الصف.
ValueError: not enough values to unpack (expected 3, got 2) عدد القيم في اليمين أقل من عدد الأسماء في اليسار. استخدم print(len(record)) لترى كم قيمة موجودة فعلاً.
ValueError: too many values to unpack (expected 2) المشكلة المعاكسة — قيم أكثر، وأسماء أقل.
أنشأت صفاً بقيمة واحدة، لكنه لم يصبح صفاً نُسيت الفاصلة. (5) رقم، و(5,) صف.
تغيّرت القائمة الموجودة داخل الصف يحمي الصف بنيته هو — أي الأشياء الموجودة داخله، وهذا لا يمكن تغييره. لكن إذا كان أحد تلك الأشياء قابلاً للتعديل بنفسه، مثل قائمة، فيمكن تعديل ما بداخله. في ([1, 2], 3) يمكنك تنفيذ append على القائمة الأولى.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
ماذا يُطبع بعد فك الحزم؟
point = (3, 7)
x, y = point
print(y, x)- A3 7
- B(3, 7)
- Cخطأ `ValueError`
- D7 3
محاولة تنفيذ append على صف. أي خطأ يظهر، ولماذا هذا الخطأ بالذات؟
point = (3, 7)
point.append(9)- Aخطأ `ValueError`
- Bلا شيء، يبقى الصف كما هو
- Cخطأ `AttributeError` — الصف لا يمكن تعديله، لذلك لا توجد فيه الدوال العضوة التي تعدّل أصلاً
- Dخطأ `TypeError` — لا يُسمح بـ `append` على الصف
الفرق بين السطرين فاصلة واحدة. ماذا تقول type()؟
one = (5)
really = (5,)
print(type(one))
print(type(really))- A<class 'int'> <class 'int'>
- B<class 'tuple'> <class 'tuple'>
- C<class 'int'> <class 'tuple'>
- D<class 'tuple'> <class 'int'>
الإجابة تتطلب حساباً
سجّل الدخول للتحقق من إجاباتك
الأسئلة موجودة أعلاه، والتفكير في إجاباتها ذهنياً هو الجزء المهم. سجّل الدخول لرؤية الإجابات والتفسيرات والتلميحات ذات المستويات الثلاثة.
دورك الآن
اكتب ملفاً باسم contacts.py فيه قائمة بخمسة أشخاص على الأقل، وكل شخص صف — الاسم، والعمر، وموضوع مفضل.
ثم:
- اطبع الجميع في أعمدة مرتبة باستخدام
for name, age, subject in contacts: - ابحث عن الأكبر سناً، وفك حزمته، واطبعه في جملة
- أنشئ قائمة بالأسماء فقط، واطبعها في سطر واحد باستخدام
join - اطبع متوسط العمر
- بدّل قيمتي متغيرين في سطر واحد
ثم اكسر البرنامج عمداً ثلاث مرات، واقرأ كل رسالة:
- حاول تغيير العمر داخل أحد الصفوف
- حاول تنفيذ
appendعلى أحد الصفوف - حاول فك حزم صف من ثلاث قيم إلى اسمين
ستظهر ثلاثة أسماء أخطاء مختلفة — TypeError وAttributeError وValueError. إذا استطعت أن تشرح لنفسك سبب كل واحد منها، فقد أتقنت الصفوف.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
سجّل الدخول لأداء الاختبار