الصفوف وفك الحزم — Tuples and Unpacking
قائمة غير قابلة للتعديل — متى تحتاج إليها، واستخدام فك الحزم للتعامل مع الأسماء بدلاً من الفهارس، والتبديل في سطر واحد، وفخ الفرق بين (5) و (5,).
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
نحتاج إلى تسجيل بيانات الطقس لثلاثة أيام — ولكل يوم نريد تسجيل الاسم ودرجة الحرارة ونسبة الرطوبة.
يمكن للقائمة أداء هذه المهمة: ["monday", 31.5, 78]. هذا الكود يعمل، لكن هناك مشكلتان مقلقتان:
أولاً، هذه القائمة قابلة للتعديل. فإذا قام جزء من الكود باستدعاء append على سجل يوم الاثنين عن طريق الخطأ، فلن تصدر بايثون أي اعتراض أو تحذير — على الرغم من أن هذا السجل لا يُفترض به أبداً أن يحتوي على أربعة عناصر! إن وضع بيانات لا ينبغي تعديلها داخل وعاء قابل للتعديل يُعد مخاطرة خفية كبيرة.
ثانياً، استرجاع هذه القيم الثلاث يتطلب كتابة record[0] و record[1] و record[2] — أي عبر الأرقام بدلاً من الأسماء الدالة. وبعد ستة أشهر، لن يتذكر أي شخص يقرأ record[1] ما إذا كانت هذه القيمة تمثل درجة الحرارة أم نسبة الرطوبة.
يحل الصف (Tuple) هاتين المشكلتين معاً: فهو بمثابة قائمة غير قابلة للتعديل إطلاقاً — كما يقدم ميزة رائعة تُعرف باسم فك الحزم (Unpacking)، والتي تتيح لك استخدام أسماء دالة بدلاً من أرقام الفهارس.
في نهاية هذا الدرس ستكون قادراً على
- إنشاء صف (tuple)، ومعرفة متى يكون الصف هو الخيار الصحيح ومتى تكون القائمة هي الأنسب
- إسناد عدة قيم إلى عدة متغيرات دفعة واحدة باستخدام فك الحزم (unpacking)
- تبديل قيمتي متغيرين في سطر برمجى واحد
- التمييز بين التعبيرين
(5)و(5,) - شرح سبب إطلاق خطأ
AttributeErrorعند محاولة استدعاءappendعلى صف
المتطلبات السابقة: ترتيب القوائم ودوالها المدمجة.
الصفوف (Tuples) — قائمة لا يمكن تعديلها
نستخدم الأقواس الدائرية العادية بدلاً من الأقواس المربعة:
point = (3, 7)
print(point)
print(point[0])
print(len(point))(3, 7)
3
2القراءة، والفهرسة، ودالة len، وشرائح الاقتطاع، والمعامل in، والتكرار بحلقة for — كل ما فعلته مع القوائم يعمل هنا تماماً. لكن هناك فارق وحيد وأساسي:
point = (3, 7)
point[0] = 5TypeError: 'tuple' object does not support item assignmentهل تبدو رسالة الخطأ هذه مألوفة؟ إنها مطابقة تماماً بالحرف لما رأيناه في الدرس السادس عند محاولة تعديل حرف داخل نص، باستثناء استبدال 'tuple' بـ 'str'. فجميع الكائنات غير القابلة للتعديل (immutable) في بايثون تشتكي بنفس اللغة والأسلوب.
كما أن الدوال التي تعدل المحتوى غير موجودة أيضاً:
point = (3, 7)
point.append(9)AttributeError: 'tuple' object has no attribute 'append'يعني الخطأ AttributeError أن "هذا النوع من الكائنات لا يملك مثل هذه الدالة التابعة". فالصف لا يمكن تعديله، ولذا فهو ببساطة لا يحتوي على الدوال التي تقوم بالتعديل — وقد ظهر هنا AttributeError وليس TypeError لأن المشكلة في الكائن نفسه الذي لا يملك هذه الصفة، وليست في العملية.
لماذا يُعد هذا ميزة وليس عيباً؟
للوهلة الأولى، قد يبدو الصف وكأنه مجرد قائمة نُزعت منها بعض الميزات. لكن انظر للأمر من زاوية أخرى: إذا كان من غير المفترض تعديل شيء ما، فإن وضعه في مكان يمنع التعديل يوقف الأخطاء قبل وقوعها.
لا معنى لإضافة رقم ثالث لنقطة إحداثيات ثنائية الأبعاد (3, 7). ولا يحتوي تاريخ مثل (2026, 9, 16) على جزء رابع. وعند الاحتفاظ بها في صفوف (tuples)، فإن أي محاولة لتغييرها ستظهر كأخطاء برمجية فورية، بدلاً من أن تتحول إلى أخطاء وسلوكيات غامضة لاحقاً في النظام.
كقاعدة عامة: عدد ثابت من العناصر، كل منها يحمل معنى مختلفاً — استخدم صَفاً (tuple). عدد غير معروف من العناصر، وجميعها من النوع نفسه — استخدم قائمة (list).
لذلك، يتم تمثيل بيانات الطقس لعدة أيام كصفوف داخل قائمة: عدد الأيام قد يزيد (قائمة)، ولكن كل سجل يومي يتكون من ثلاثة عناصر ثابتة تماماً (صف).
فك الحزم (Unpacking) — حيث تكمن الفائدة الحقيقية
point = (3, 7)
x, y = point
print(x)
print(y)3
7اسمان وقيمتان في سطر واحد! وأصبح المتغير الآن اسمه y بدلاً من point[1] — وهو اسم واضح يشرح نفسه بنفسه بمجرد قراءته.
يجب أن يكون عدد المتغيرات في الطرف الأيسر مساوياً تماماً لعدد القيم في الطرف الأيمن:
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'>ما يصنع الصف في الحقيقة هو الفاصلة (comma)، وليس الأقواس. الأقواس موجودة من أجل سهولة القراءة وتوضيح الحدود. وبمجرد معرفة ذلك، لن تبدو العبارة a, b = b, a كالسحر — فالطرف الأيمن b, a هو ببساطة صف (tuple).
ومن هذه النقطة بالتحديد ينشأ فخ شهير:
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 صفوفاً (tuples). وحين كتبنا for position, item in enumerate(...) في الدرس السابق، كنا في الواقع نقوم بفك حزمها — فقط لم نكن نعرف الاسم التقني للعملية حينها!
التحويل بين القوائم والصفوف
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 في الدرس السادس، فإن كل ما يلي النقطتين الرأسيتين هو تعليمات تنسيق.
البحث عن اليوم الأكثر حرارة تطلب كتابة 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 على القائمة الأولى دون مشاكل.
Step 4 of 6 — Predict
Check your understanding
What is printed after the unpacking?
point = (3, 7)
x, y = point
print(y, x)- A7 3
- B3 7
- C(3, 7)
- DA `ValueError`
An attempt to append to a tuple. Which error comes, and why that one?
point = (3, 7)
point.append(9)- AAn `AttributeError` — a tuple cannot change, so the changing methods simply do not exist on it
- BA `TypeError` — `append` is not allowed on a tuple
- CNothing happens; the tuple is unchanged
- DA `ValueError`
The two lines differ by one comma. What does type() report?
one = (5)
really = (5,)
print(type(one))
print(type(really))- A<class 'int'> <class 'tuple'>
- B<class 'tuple'> <class 'tuple'>
- C<class 'int'> <class 'int'>
- D<class 'tuple'> <class 'int'>
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
اكتب ملفاً باسم contacts.py يحتوي على قائمة تضم خمسة أشخاص على الأقل، بحيث يمثل كل شخص صَفاً (tuple) يحتوي على: الاسم، والعمر، والمادة المفضلة.
ثم قم بما يلي:
- اطبع جميع الأشخاص في أعمدة محاذية ومنسقة، باستخدام حلقة تكرارية:
for name, age, subject in contacts: - ابحث عن الشخص الأكبر سناً، وفك حزمته، واطبع جملة تصفه
- أنشئ قائمة بالأسماء فقط، واطبعها في سطر واحد مفصولة بفواصل باستخدام
join - اطبع متوسط الأعمار
- بَدّل بين قيمتي متغيرين في سطر برمجى واحد وأثبت نجاح التبديل
ثم تسبب عمداً في ثلاثة أخطاء واقرأ رسائلها بعناية:
- حاول تعديل العمر داخل أحد الصفوف
- حاول استدعاء
appendعلى صف - حاول فك حزمة صف يتكون من ثلاث قيم في متغيرين فقط
ستحصل على ثلاثة أخطاء مختلفة — TypeError و AttributeError و ValueError. وعندما تكون قادراً على شرح الفرق بين كل منها ولماذا حدثت، ستكون قد أتقنت الصفوف وفك الحزم تماماً.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz