الفصل 14

الصفوف وفك الحزم — Tuples and Unpacking

قائمة غير قابلة للتعديل — متى تحتاج إليها، واستخدام فك الحزم للتعامل مع الأسماء بدلاً من الفهارس، والتبديل في سطر واحد، وفخ الفرق بين (5) و (5,).

30 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

نحتاج إلى تسجيل بيانات الطقس لثلاثة أيام — ولكل يوم نريد تسجيل الاسم ودرجة الحرارة ونسبة الرطوبة.

يمكن للقائمة أداء هذه المهمة: ["monday", 31.5, 78]. هذا الكود يعمل، لكن هناك مشكلتان مقلقتان:

أولاً، هذه القائمة قابلة للتعديل. فإذا قام جزء من الكود باستدعاء append على سجل يوم الاثنين عن طريق الخطأ، فلن تصدر بايثون أي اعتراض أو تحذير — على الرغم من أن هذا السجل لا يُفترض به أبداً أن يحتوي على أربعة عناصر! إن وضع بيانات لا ينبغي تعديلها داخل وعاء قابل للتعديل يُعد مخاطرة خفية كبيرة.

ثانياً، استرجاع هذه القيم الثلاث يتطلب كتابة record[0] و record[1] و record[2] — أي عبر الأرقام بدلاً من الأسماء الدالة. وبعد ستة أشهر، لن يتذكر أي شخص يقرأ record[1] ما إذا كانت هذه القيمة تمثل درجة الحرارة أم نسبة الرطوبة.

يحل الصف (Tuple) هاتين المشكلتين معاً: فهو بمثابة قائمة غير قابلة للتعديل إطلاقاً — كما يقدم ميزة رائعة تُعرف باسم فك الحزم (Unpacking)، والتي تتيح لك استخدام أسماء دالة بدلاً من أرقام الفهارس.

في نهاية هذا الدرس ستكون قادراً على

  • إنشاء صف (tuple)، ومعرفة متى يكون الصف هو الخيار الصحيح ومتى تكون القائمة هي الأنسب
  • إسناد عدة قيم إلى عدة متغيرات دفعة واحدة باستخدام فك الحزم (unpacking)
  • تبديل قيمتي متغيرين في سطر برمجى واحد
  • التمييز بين التعبيرين (5) و (5,)
  • شرح سبب إطلاق خطأ AttributeError عند محاولة استدعاء append على صف

المتطلبات السابقة: ترتيب القوائم ودوالها المدمجة.


الصفوف (Tuples) — قائمة لا يمكن تعديلها

نستخدم الأقواس الدائرية العادية بدلاً من الأقواس المربعة:

python
point = (3, 7)

print(point)
print(point[0])
print(len(point))
text
(3, 7)
3
2

القراءة، والفهرسة، ودالة len، وشرائح الاقتطاع، والمعامل in، والتكرار بحلقة for — كل ما فعلته مع القوائم يعمل هنا تماماً. لكن هناك فارق وحيد وأساسي:

python
point = (3, 7)
point[0] = 5
text
TypeError: 'tuple' object does not support item assignment

هل تبدو رسالة الخطأ هذه مألوفة؟ إنها مطابقة تماماً بالحرف لما رأيناه في الدرس السادس عند محاولة تعديل حرف داخل نص، باستثناء استبدال 'tuple' بـ 'str'. فجميع الكائنات غير القابلة للتعديل (immutable) في بايثون تشتكي بنفس اللغة والأسلوب.

كما أن الدوال التي تعدل المحتوى غير موجودة أيضاً:

python
point = (3, 7)
point.append(9)
text
AttributeError: 'tuple' object has no attribute 'append'

يعني الخطأ AttributeError أن "هذا النوع من الكائنات لا يملك مثل هذه الدالة التابعة". فالصف لا يمكن تعديله، ولذا فهو ببساطة لا يحتوي على الدوال التي تقوم بالتعديل — وقد ظهر هنا AttributeError وليس TypeError لأن المشكلة في الكائن نفسه الذي لا يملك هذه الصفة، وليست في العملية.

لماذا يُعد هذا ميزة وليس عيباً؟

للوهلة الأولى، قد يبدو الصف وكأنه مجرد قائمة نُزعت منها بعض الميزات. لكن انظر للأمر من زاوية أخرى: إذا كان من غير المفترض تعديل شيء ما، فإن وضعه في مكان يمنع التعديل يوقف الأخطاء قبل وقوعها.

لا معنى لإضافة رقم ثالث لنقطة إحداثيات ثنائية الأبعاد (3, 7). ولا يحتوي تاريخ مثل (2026, 9, 16) على جزء رابع. وعند الاحتفاظ بها في صفوف (tuples)، فإن أي محاولة لتغييرها ستظهر كأخطاء برمجية فورية، بدلاً من أن تتحول إلى أخطاء وسلوكيات غامضة لاحقاً في النظام.

كقاعدة عامة: عدد ثابت من العناصر، كل منها يحمل معنى مختلفاً — استخدم صَفاً (tuple). عدد غير معروف من العناصر، وجميعها من النوع نفسه — استخدم قائمة (list).

لذلك، يتم تمثيل بيانات الطقس لعدة أيام كصفوف داخل قائمة: عدد الأيام قد يزيد (قائمة)، ولكن كل سجل يومي يتكون من ثلاثة عناصر ثابتة تماماً (صف).

فك الحزم (Unpacking) — حيث تكمن الفائدة الحقيقية

python
point = (3, 7)
x, y = point

print(x)
print(y)
text
3
7

اسمان وقيمتان في سطر واحد! وأصبح المتغير الآن اسمه y بدلاً من point[1] — وهو اسم واضح يشرح نفسه بنفسه بمجرد قراءته.

يجب أن يكون عدد المتغيرات في الطرف الأيسر مساوياً تماماً لعدد القيم في الطرف الأيمن:

python
point = (3, 7)
x, y, z = point
text
ValueError: not enough values to unpack (expected 3, got 2)

رسالة الخطأ هذه واضحة ومفيدة بشكل استثنائي — فهي تخبرك بالعدد المتوقع وبالعدد الفعلي الذي وصلها.

فك الحزم لا يقتصر على الصفوف فقط؛ فهو يعمل على القوائم أيضاً. لكنه أكثر فائدة وشيوعاً مع الصفوف لأن عدد عناصرها يكون معلوماً ومحدداً مسبقاً.

التبديل في سطر واحد

تحتاج اللغات الأخرى إلى متغير مؤقت ثالث للتبديل بين قيمتين. بايثون لا تحتاج ذلك:

python
a = 1
b = 2

a, b = b, a
print(a, b)
text
2 1

آلية عمل هذا السطر تستحق الفهم: تقوم بايثون بحساب وتقييم الطرف الأيمن كاملاً أولاً، فتنشئ صفاً (2, 1)، ثم بعد ذلك تقوم بفك حزمته وإسناد قيمه إلى الطرف الأيسر. لذا، حتى مع تغير قيمة a، فإن القيمة القديمة لـ b تكون قد حُفظت بالفعل مسبقاً، ولن يضيع أي شيء.

الأقواس ليست إلزامية في الواقع!

python
point = 3, 7
print(point)
print(type(point))
text
(3, 7)
<class 'tuple'>

ما يصنع الصف في الحقيقة هو الفاصلة (comma)، وليس الأقواس. الأقواس موجودة من أجل سهولة القراءة وتوضيح الحدود. وبمجرد معرفة ذلك، لن تبدو العبارة a, b = b, a كالسحر — فالطرف الأيمن b, a هو ببساطة صف (tuple).

ومن هذه النقطة بالتحديد ينشأ فخ شهير:

python
one = (5)
really = (5,)

print(type(one))
print(type(really))
print(len(really))
text
<class 'int'>
<class 'tuple'>
1

التعبير (5) ليس صفاً — بل هو مجرد رقم محاط بقوسين، تماماً مثل (2 + 3). وإنشاء صف يحتوي على عنصر واحد فقط يتطلب وجود فاصلة بعده: (5,). قد تبدو للوهلة الأولى كخطأ مطبعي، لكن تلك الفاصلة تصنع كل الفارق.

الصفوف داخل الحلقات التكرارية

python
people = [("rafi", 22), ("ahmed", 35)]

for name, age in people:
    print(f"{name} is {age}")
text
rafi is 22
ahmed is 35

تقوم العبارة for name, age in people بفك حزمة كل صف مباشرة إلى متغيرين. وداعاً لكتابة person[0] و person[1] — فالكود الآن يعبر بوضوح عن معناه لكل من يقرؤه.

لقد استخدمت هذا النمط بالفعل دون أن تدري:

python
stock = ["pen", "bag"]

for pair in enumerate(stock):
    print(pair)
text
(0, 'pen')
(1, 'bag')

تُرجع الدالة enumerate صفوفاً (tuples). وحين كتبنا for position, item in enumerate(...) في الدرس السابق، كنا في الواقع نقوم بفك حزمها — فقط لم نكن نعرف الاسم التقني للعملية حينها!

التحويل بين القوائم والصفوف

python
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))
text
[3, 7, 9]
(3, 7)
[3, 7]
(3, 7, 9)

تنشئ الدوال list() و tuple() كائناً من النوع الآخر — كائناً جديداً في كل مرة، وتترك الأصل دون مساس. وهذه هي الطريقة التي يمكنك من خلالها "تعديل" صف: حوّله إلى قائمة، عدّل القائمة، ثم أعد تحويلها إلى صف مرة أخرى.


مثال متكامل

weather.py:

python
# 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))
text
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 على القائمة الأولى دون مشاكل.