البرمجة بلغة بايثون · الفصل 6 — بنى المعطياتמבני נתונים · יסודות התכנות בשפת Python⌂📚🏠
الفصل 6

بنى المعطيات

מבני נתונים
· 30 ساعات تعليمية

حتى الآن حفظ كل متغيّر قيمة واحدة. لكن المحطة التي تقيس كل ساعة، والمختبر الذي يسجّل نتائج طلابه، والجهاز الذي يأخذ ألف عينة من إشارة، كلها تحتاج إلى حفظ قيم كثيرة معا والعودة إليها لاحقا. في هذا الفصل نتعرف إلى القائمة، وهي أكثر بنى المعطيات استعمالا في بايثون، وإلى الحلقة for التي تمر عليها، ثم نكتب بأنفسنا خوارزميات البحث والفرز.

في هذا الفصل:
  • نبني قائمة، نصل إلى عناصرها بالمؤشّر والقصّ، ونغيّرها.
  • نستعمل قائمة عدّادات وقائمة مُجمِّعات.
  • نمر على القوائم بالحلقة for، بالعناصر وبالمؤشّرات.
  • نستعمل عمليّات القائمة، ونبني قائمة من قوائم.
  • نكتب بأنفسنا خوارزميات القيمة القصوى والدمج والفرز.
  • نميّز بين القائمة والصفّ الثابت tuple، ونعرف متى نختار كلا منهما.

6.1 مفهوم بنية المعطيات – היכרות עם המושג מבנה נתונים

في الفصل 4 قرأنا سلسلة قياسات بحلقة وحسبنا متوسطها. في كل دورة كانت القراءة الجديدة تحل مكان السابقة في المتغيّر نفسه. فإذا سألنا بعد الحلقة: أي القراءات أعلى من المتوسط؟ لم نجد جوابا، لأن القراءات ضاعت. لا نعرف المتوسط إلا في النهاية، وفي النهاية لم يبق لدينا سوى آخر قراءة.

مثال 6.1
لدينا أربع قراءات لجهد بطارية. نطبع القراءات الأعلى من متوسطها، ولا نملك إلا المتغيّرات المنفصلة.
الحل:
r1 = 12.1
r2 = 11.6
r3 = 12.4
r4 = 11.9
average = (r1 + r2 + r3 + r4) / 4
if r1 > average:
    print(r1)
if r2 > average:
    print(r2)
if r3 > average:
    print(r3)
if r4 > average:
    print(r4)
المُخرَج
12.1
12.4

البرنامج صحيح، لكنه لا يكبر معنا. مئة قراءة تعني مئة متغيّر ومئة أمر شرطي. ولا يمكننا أن نترك عدد القراءات للمستخدم، لأن أسماء المتغيّرات تُكتب مسبقا في البرنامج.

بنية المعطيات (מבנה נתונים): طريقة لحفظ مجموعة من القيم تحت اسم واحد، بترتيب يتيح لنا الوصول إلى كل قيمة منها والعمل عليها.

البنية الأولى التي نتعلمها هي القائمة (רשימה). نكتب القيم بين قوسين مربعين، وتفصل بينها فواصل. وكل قيمة في البنية اسمها عنصر (איבר):

readings = [12.1, 11.6, 12.4, 11.9]
print(readings)
print(readings[0])
print(len(readings))
print(round(sum(readings) / len(readings), 2))
المُخرَج
[12.1, 11.6, 12.4, 11.9]
12.1
4
12.0

الآن بقيت القراءات الأربع محفوظة تحت اسم واحد. وسنعود إلى سؤال القراءات الأعلى من المتوسط في البند 6.7، فنحلّه بأربعة أسطر مهما كان عددها. نتعلم في هذا الفصل بنيتين:

البنيةكتابتهاهل تتغيّر بعد إنشائها؟مثال على استعمالها
القائمة list[12.1, 11.6]نعم: نضيف ونحذف ونغيّرسجل قراءات يكبر مع الوقت
الصفّ الثابت tuple(8, 12.4)لا: تبقى كما أنشأناهاقراءة واحدة: ساعتها وجهدها معا
ملاحظة: في نص الامتحان العربي يُسمّى العنصر «حدّا». نبقى في هذا الكتاب مع كلمة «عنصر»، ونتذكر أنهما الشيء نفسه.
للمعلم: في منهاج الصف العاشر تنتهي المادة في منتصف هذا الفصل، ويكمل الصف الحادي عشر من هناك.
سؤال 6.1: نقرأ قياسات في حلقة ونحفظ آخر قراءة في متغيّر واحد فقط. أي الأسئلة التالية نستطيع أن نجيب عنها من دون قائمة؟
(أ) ما مجموع القراءات؟
(ب) كم قراءة تزيد على المتوسط؟
(ج) ما أعلى قراءة؟

6.2 تهيئة القائمة والعمل على عناصرها – אתחול, רשימה ריקה, list, הדפסה, פעולות על איברים, רשימת מונים ורשימת צוברים (list)

نهيّئ قائمة بكتابة عناصرها بين قوسين مربعين. يمكن أن تكون العناصر أعدادا أو جملا نصية، ويمكن أن نخلط بينها، لكن القائمة التي عناصرها من نوع واحد أسهل في الاستعمال. نوع القائمة هو list، والدالة len تعطي عدد عناصرها.

volts = [12.1, 11.6, 12.4]
parts = ["R1", "C4", "D2", "LED"]
mixed = ["R1", 470, 0.25]
print(volts)
print(parts)
print(mixed)
print(type(volts), len(parts))
المُخرَج
[12.1, 11.6, 12.4]
['R1', 'C4', 'D2', 'LED']
['R1', 470, 0.25]
<class 'list'> 4

القائمة الفارغة (רשימה ריקה) لا عناصر فيها، وطولها صفر. نكتبها [] أو list()، وهي نقطة البداية عندما نعبّئ القائمة أثناء تشغيل البرنامج. والدالة list تحوّل إلى قائمة أي قيمة يمكن المرور على أجزائها: جملة نصية، أو range، أو صفّا ثابتا، وسنتعرف إلى الأخيرين في هذا الفصل. أما العمليّة split من الفصل 5 فكانت تعطينا قائمة منذ البداية:

log = []
print(log, len(log))
print(list())
print(list("ADC"))
print(list(range(4)))
print("BAT=12.42;TMP=31.5".split(";"))
المُخرَج
[] 0
[]
['A', 'D', 'C']
[0, 1, 2, 3]
['BAT=12.42', 'TMP=31.5']
خطأ شائع: العدد ليس مجموعة قيم، ولذلك يوقف list(5) البرنامج. لقائمة فيها العدد وحده نكتب [5]. ولا نسمّي متغيّرا list، لأننا بذلك نفقد الدالة الجاهزة التي تحمل هذا الاسم.

الدالة print تطبع القائمة كلها بقوسيها وفواصلها. وللوصول إلى عنصر نكتب مؤشّره بين قوسين مربعين، كما في الجمل النصية. والعنصر يتصرف مثل متغيّر عادي: نسند إليه قيمة جديدة ونحسب به. والدوال sum وmax وmin من الفصل 2 تعمل على القائمة كلها:

loads = [60, 100, 40]
print(loads)
print(loads[0], "W and", loads[1], "W")
loads[1] = 75
loads[2] += 20
loads[0] = loads[0] * 2
print(loads)
print(sum(loads), max(loads), min(loads))
المُخرَج
[60, 100, 40]
60 W and 100 W
[120, 75, 60]
255 120 60

هنا يظهر فرق أساسي عن الجملة النصية: القائمة تتغيّر في مكانها، أما حرف في جملة نصية فلا يمكن تغييره.

قائمة العدّادات – רשימת מונים:

في الفصل 4 عددنا بعدّاد واحد. فإذا أردنا أن نعدّ عشرة أشياء مختلفة احتجنا إلى عشرة عدّادات. قائمة العدّادات (רשימת מונים) تجمعها كلها في قائمة واحدة، وتبدأ كلها من الصفر. وفكرتها الجميلة أن القيمة التي نقرأها هي نفسها المؤشّر إلى عدّادها، فلا نحتاج إلى سلسلة شروط.

مثال 6.2
في شفرة ألوان المقاومات لكل رقم من 0 إلى 9 لون: الأسود 0، البني 1، الأحمر 2، وهكذا. نقرأ أرقام الأشرطة التي ظهرت على مقاومات في المخزن حتى القيمة −1، ونعدّ كم مرة ظهر كل رقم.
الحل:
counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
digit = int(input("Digit (-1 to stop): "))
while digit != -1:
    counts[digit] += 1
    digit = int(input("Digit (-1 to stop): "))
print(counts)
print("Red bands:", counts[2])
المُخرَج
Digit (-1 to stop): 2
Digit (-1 to stop): 7
Digit (-1 to stop): 2
Digit (-1 to stop): 1
Digit (-1 to stop): 0
Digit (-1 to stop): 2
Digit (-1 to stop): -1
[1, 1, 3, 0, 0, 0, 0, 1, 0, 0]
Red bands: 3
counts01112330405060718090
قائمة العدّادات بعد التشغيل: المؤشّر هو الرقم، ولكل رقم لونه، والخانة الملوّنة هي عدّاد زاد

الأمر counts[digit] += 1 يزيد العدّاد المناسب مباشرة. لو كتبنا البرنامج بلا قائمة لاحتجنا إلى عشرة متغيّرات وعشرة شروط.

تنبيه: المُدخَل هنا يصبح مؤشّرا، ولذلك فإن رقما خارج المجال، مثل 12، يوقف البرنامج. وعندما تبدأ القيم من 1 لا من 0 نطرح 1: الزر رقم 1 عدّاده في الخانة 0. وهذا بالضبط ما نحتاج إليه في سؤال البجروت في نهاية الفصل.
سؤال 6.2: لوحة تحكم فيها أربعة أزرار مرقّمة من 1 إلى 4. ماذا يطبع البرنامج للمُدخَلات 1, 3, 3, 4, 1, 3, 0؟
presses = [0, 0, 0, 0]
button = int(input("Button: "))
while button != 0:
    presses[button - 1] += 1
    button = int(input("Button: "))
print(presses)

قائمة المُجمِّعات – רשימת צוברים:

قائمة المُجمِّعات (רשימת צוברים) تشبه قائمة العدّادات، لكننا نضيف إلى الخانة مقدارا ما بدل أن نضيف 1. المؤشّر يحدد إلى أين نضيف، والمُدخَل الثاني يحدد كم نضيف.

مثال 6.3
ثلاثة فنيين في ورشة، أرقامهم 0 و1 و2. نقرأ تقارير العمل حتى القيمة −1: رقم الفني وعدد الساعات. نطبع مجموع ساعات كل فني ومجموع الساعات كلها.
الحل:
hours = [0, 0, 0]
tech = int(input("Technician (-1 to stop): "))
while tech != -1:
    hours[tech] += float(input("Hours: "))
    tech = int(input("Technician (-1 to stop): "))
print(hours)
print("Total:", sum(hours))
المُخرَج
Technician (-1 to stop): 0
Hours: 6.5
Technician (-1 to stop): 2
Hours: 8
Technician (-1 to stop): 0
Hours: 3
Technician (-1 to stop): -1
[9.5, 0, 8.0]
Total: 17.5

الفني رقم 0 ورد مرتين، فجُمعت ساعاته في خانته.

6.3 البنية التسلسلية للقائمة والوصول بالمؤشّر – המבנה הסידורי של List, גישה לאיבר באמצעות אינדקס

عناصر القائمة مرتبة، ولكل عنصر رقم يدل على مكانه هو المؤشّر (אינדקס). يبدأ العد من الصفر كما في الجمل النصية، فالعنصر الأول مؤشّره 0، والأخير مؤشّره أقل من الطول بواحد. ويمكن أيضا أن نعدّ من النهاية بمؤشّر سالب: [-1] هو العنصر الأخير، و[-2] الذي قبله.

samples0.20-51.41-43.12-34.83-22.64-1
لكل عنصر مؤشّران: موجب فوق الخانة، وسالب تحتها
samples = [0.2, 1.4, 3.1, 4.8, 2.6]
print(samples[0], samples[2], samples[4])
print(samples[-1], samples[-5])
samples[-1] = 0.0
print(samples)
المُخرَج
0.2 3.1 2.6
2.6 0.2
[0.2, 1.4, 3.1, 4.8, 0.0]

المؤشّر السالب يعفينا من كتابة samples[len(samples) - 1]، ويعمل مهما كان طول القائمة، وهو يصلح للتغيير أيضا.

خطأ شائع: المؤشّر الأعلى في قائمة من خمسة عناصر هو 4، لا 5. طلب عنصر غير موجود يوقف البرنامج:
samples = [0.2, 1.4, 3.1, 4.8, 2.6]
print(samples[5])
المُخرَج
IndexError: list index out of range
مثال 6.4
المؤشّر قد يكون متغيّرا، وهنا تكمن قوة القائمة: بمتغيّر يكبر في كل دورة نمر على العناصر كلها. نعدّ بالحلقة while من الفصل 4 العينات التي تزيد على 2.5 V.
الحل:
samples = [0.2, 1.4, 3.1, 4.8, 2.6]
high = 0
i = 0
while i < len(samples):
    if samples[i] > 2.5:
        high += 1
    i += 1
print("Above 2.5 V:", high)
المُخرَج
Above 2.5 V: 3

الشرط هو i < len(samples). لو كتبنا <= لأضفنا دورة بعد العنصر الأخير، ولتوقف البرنامج بالخطأ نفسه.

تدريب سريع — المؤشّر، للقائمة v = [5, 10, 15, 20, 25, 30]
1v[0]
2v[3]
3v[-1]
4v[-2]
5v[len(v) - 2]
6v[1] + v[-1]
الإجابات: 1 5 2 20 3 30 4 25 5 25 6 40

6.4 قصّ القوائم – חיתוך רשימות (slicing)

القصّ (חיתוך) يعطينا جزءا من القائمة كقائمة جديدة. نكتب s[start:stop:step]: نبدأ من المؤشّر start، ونتوقف قبل المؤشّر stop، ونتقدم بخطوة step. هذه هي القواعد نفسها التي عرفناها مع الجمل النصية:

s100-6201-5302-4403-3504-2605-1s[1:4]
المؤشّر 4 هو حد التوقف ولا يدخل في الناتج، ولذلك نحصل على ثلاثة عناصر
s = [10, 20, 30, 40, 50, 60]
print(s[1:4])
print(s[:3], s[3:])
print(s[-2:])
print(s[::2])
print(s[::-1])
print(s[2:100])
print(s[4:2])
print(s)
المُخرَج
[20, 30, 40]
[10, 20, 30] [40, 50, 60]
[50, 60]
[10, 30, 50]
[60, 50, 40, 30, 20, 10]
[30, 40, 50, 60]
[]
[10, 20, 30, 40, 50, 60]
مثال 6.5
جهاز قياس يحتاج إلى لحظات حتى يستقر، فأول عينتين غير موثوقتين. نحذفهما ونحسب متوسط الباقي. ثم نخفّض معدّل العينات إلى النصف، بأخذ عينة من كل اثنتين.
الحل:
samples = [0.1, 0.9, 2.4, 2.5, 2.6, 2.4, 2.5]
stable = samples[2:]
print(stable)
print(round(sum(stable) / len(stable), 2))
print(stable[::2])
المُخرَج
[2.4, 2.5, 2.6, 2.4, 2.5]
2.48
[2.4, 2.6, 2.5]

الإسناد لا ينسخ القائمة:

خطأ شائع: نظن أن b = a يصنع نسخة، فنغيّر b ونتوقع أن تبقى a كما هي. لكن هذا الأمر لا ينشئ قائمة ثانية، بل يعطي القائمة نفسها اسما آخر، وكل تغيير عبر أحد الاسمين يظهر في الآخر:
a = [12, 9, 5]
b = a
b[0] = 99
print(a)
print(b)
المُخرَج
[99, 9, 5]
[99, 9, 5]

حين نريد نسخة حقيقية نستعمل القصّ a[:]، أو الدالة list(a)، فتنشأ قائمة جديدة في الذاكرة:

a = [12, 9, 5]
b = a[:]
b[0] = 99
print(a)
print(b)
المُخرَج
[12, 9, 5]
[99, 9, 5]
b = aab[99, 9, 5]b = a[:]ab[12, 9, 5][99, 9, 5]
إلى اليسار: اسمان وقائمة واحدة. إلى اليمين: لكل اسم قائمته. كل من الشكلين بعد الأمر b[0] = 99
تدريب سريع — القصّ، للقائمة v = [1, 2, 3, 4, 5, 6]
1v[2:4]
2v[:2]
3v[4:]
4v[-3:-1]
5v[1::2]
6v[::-2]
الإجابات: 1 [3, 4] 2 [1, 2] 3 [5, 6] 4 [4, 5] 5 [2, 4, 6] 6 [6, 4, 2]
سؤال 6.3: ماذا يطبع البرنامج التالي؟
a = [1, 2, 3]
b = a
c = a[:]
a[0] = 7
b[1] = 8
c[2] = 9
print(a)
print(b)
print(c)

6.5 إضافة عناصر إلى القائمة – הוספת איברים לרשימה

القائمة تكبر أثناء تشغيل البرنامج. العمليّة append تضيف عنصرا واحدا إلى النهاية، وهي الطريقة التي نستعملها في أغلب الأحيان. والمعامل + يصل قائمتين فينتج قائمة جديدة، و+= يضيف قائمة إلى نهاية القائمة نفسها. والمعامل * يكرر القائمة، وهذه أقصر طريقة لتهيئة قائمة عدّادات: بدل عشرة الأصفار في البند 6.2 نكتب [0] * 10.

log = [12.1]
log.append(11.8)
print(log)
log = log + [11.9, 12.0]
print(log)
log += [12.2]
print(log)
print([0] * 5)
المُخرَج
[12.1, 11.8]
[12.1, 11.8, 11.9, 12.0]
[12.1, 11.8, 11.9, 12.0, 12.2]
[0, 0, 0, 0, 0]
مثال 6.6
نقرأ قدرات أحمال كهربائية حتى القيمة 0، ولا نعرف مسبقا كم حملا سيدخل المستخدم. نبدأ بقائمة فارغة ونضيف إليها في كل دورة.
الحل:
loads = []
watts = float(input("Load W (0 to stop): "))
while watts != 0:
    loads.append(watts)
    watts = float(input("Load W (0 to stop): "))
print(loads)
print("Count:", len(loads))
print("Total:", sum(loads))
المُخرَج
Load W (0 to stop): 60
Load W (0 to stop): 100
Load W (0 to stop): 40.5
Load W (0 to stop): 0
[60.0, 100.0, 40.5]
Count: 3
Total: 200.5

هذا النمط يتكرر كثيرا: قائمة فارغة قبل الحلقة، وappend داخلها. وهو يحل مشكلة بداية الفصل، إذ لم يعد عدد القراءات يُكتب مسبقا في البرنامج.

خطأ شائع: المعامل + يصل قائمة بقائمة فقط، ولذلك نحيط العنصر الجديد بقوسين مربعين: log + 11.8 يوقف البرنامج، وlog + [11.8] صحيح. وكذلك لا ننشئ عنصرا جديدا بالإسناد إلى مؤشّر غير موجود: في قائمة فارغة يوقف log[0] = 12.1 البرنامج، والإضافة تتم بالعمليّة append.
سؤال 6.4: ماذا يطبع البرنامج التالي؟
a = [1]
a.append(2)
b = a + [3]
a += [4, 5]
print(a)
print(b)
print([7] * 2 + [8])

6.6 استعمال range لتهيئة قائمة – שימוש ב-range לאתחול רשימה

الدالة range تولّد سلسلة أعداد صحيحة. نحوّلها إلى قائمة بالدالة list، فنحصل على قائمة طويلة من دون أن نكتب عناصرها. ولها ثلاثة أشكال:

الأمرعمله
range(stop)من 0 حتى ما قبل stop
range(start, stop)من start حتى ما قبل stop
range(start, stop, step)من start حتى ما قبل stop، بخطوة step، وقد تكون سالبة
print(list(range(5)))
print(list(range(2, 8)))
print(list(range(0, 50, 10)))
print(list(range(5, 0, -1)))
print(range(5))
المُخرَج
[0, 1, 2, 3, 4]
[2, 3, 4, 5, 6, 7]
[0, 10, 20, 30, 40]
[5, 4, 3, 2, 1]
range(0, 5)

الحد stop لا يدخل في السلسلة أبدا، كما في القصّ. والسطر الأخير يبيّن أن range وحدها ليست قائمة، بل وصف لسلسلة أعداد تُولَّد عند الحاجة، ولذلك كتبنا list كي نراها.

مثال 6.7
نفحص استجابة مضخّم عند ترددات من 100 Hz حتى 1000 Hz، بخطوة 100 Hz. نبني قائمة الترددات، ثم قائمة بأرقام ثماني قنوات لمحوّل تماثلي-رقمي، مرقّمة من 1.
الحل:
freqs = list(range(100, 1100, 100))
print(freqs)
channels = list(range(1, 9))
print(channels)
المُخرَج
[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]
[1, 2, 3, 4, 5, 6, 7, 8]

الحد الأعلى في السطر الأول هو 1100 كي يدخل 1000 في القائمة.

خطأ شائع: الدالة range تقبل أعدادا صحيحة فقط، ولذلك يوقف range(0, 1, 0.1) البرنامج: لا توجد خطوة عشرية.
تدريب سريع — الدالة range
1list(range(4))
2list(range(1, 4))
3list(range(0, 9, 3))
4list(range(10, 4, -2))
الإجابات: 1 [0, 1, 2, 3] 2 [1, 2, 3] 3 [0, 3, 6] 4 [10, 8, 6]

6.7 الحلقة for على عناصر القائمة – לולאת for העוברת על איברי רשימה (for)

في البند 6.3 مررنا على القائمة بالحلقة while: هيّأنا مؤشّرا، فحصناه، ورفعناه في كل دورة. هذا المرور شائع جدا، ولذلك توجد له حلقة خاصة هي for. نكتبها هكذا:

for item in sequence:
    statement
    statement
next_statement
هل بقي عنصر؟item = العنصر التاليجسم الحلقةالأمر التاليTrueFalse
مخطط انسياب الحلقة for: تدور ما دام في القائمة عنصر لم نمرّ عليه
volts = [12.1, 11.6, 12.4]
for v in volts:
    print("Reading:", v)
print("Done")
المُخرَج
Reading: 12.1
Reading: 11.6
Reading: 12.4
Done
متابعة البرنامج: في كل دورة يحمل v العنصر التالي
الأمرvoltsvالمُخرَج
volts = [12.1, 11.6, 12.4][12.1, 11.6, 12.4]
for v in volts:12.1
print("Reading:", v)Reading: 12.1
for v in volts:11.6
print("Reading:", v)Reading: 11.6
for v in volts:12.4
print("Reading:", v)Reading: 12.4
for v in volts:
print("Done")Done

ظهر السطر for في الجدول أربع مرات لقائمة من ثلاثة عناصر: في المرة الرابعة لم يبق عنصر، فخرجت الحلقة إلى الأمر التالي.

مثال 6.8
نعود إلى السؤال الذي بدأنا به الفصل: نطبع القراءات الأعلى من متوسطها. نقارن بين هذا البرنامج وبين البرنامج ذي الأوامر الشرطية الأربعة في البند 6.1.
الحل:
readings = [12.1, 11.6, 12.4, 11.9]
average = sum(readings) / len(readings)
for r in readings:
    if r > average:
        print(r)
المُخرَج
12.1
12.4

يبقى البرنامج كما هو لأربع قراءات ولأربعة آلاف قراءة، ولا يتغيّر فيه سوى السطر الأول.

خطأ شائع: يحمل متغيّر الحلقة قيمة العنصر، وإسناد قيمة جديدة إليه لا يغيّر القائمة. وهو بعد انتهاء الحلقة يبقى على آخر قيمة حملها. لتغيير العناصر نحتاج إلى مؤشّراتها، وهذا موضوع البند التالي:
volts = [12, 9, 5]
for v in volts:
    v = v * 2
print(volts)
print(v)
المُخرَج
[12, 9, 5]
10

الحلقة for تعمل أيضا على كل ما يمكن المرور على أجزائه، ومنه الجملة النصية: for ch in "ADC":. وعلى قائمة فارغة لا يُنفَّذ جسمها ولا مرة، ولا يحدث أي خطأ.

سؤال 6.5: ماذا يطبع البرنامج التالي؟ نبني جدول متابعة ونفحص.
items = [4, 7, 2, 9]
s = 0
for x in items:
    if x % 2 == 1:
        s += x
    print(s)

6.8 الحلقة for مع range – שילוב של לולאת for עם range

الدالة range تعطي سلسلة أعداد، والحلقة for تمر على السلسلة. عندما نجمع بينهما يصبح لدينا عدّاد جاهز، وهذا أبسط تكرار لعدد معروف من الدورات. وبهذه الطريقة نقرأ أيضا عددا معروفا من القيم، من دون قيمة توقف:

for i in range(4):
    print(i, i * 5)
المُخرَج
0 0
1 5
2 10
3 15
currents = []
for k in range(3):
    currents.append(float(input("Current: ")))
print(currents)
print(max(currents))
المُخرَج
Current: 1.5
Current: 0.75
Current: 2.25
[1.5, 0.75, 2.25]
2.25

الاستعمال الأهم هو المرور على مؤشّرات القائمة: for i in range(len(v)): يعطي 0 ثم 1 حتى آخر مؤشّر، مهما كان طول القائمة. نحتاج إلى ذلك في ثلاث حالات، والحلقة على العناصر لا تكفي في أي منها.

مثال 6.9
الحالة الأولى: نغيّر العناصر. عندنا قراءات محوّل ذي عشر خانات، ونريد أن نحوّل كلا منها إلى جهد في القائمة نفسها.
الحل:
adc = [0, 256, 512, 1023]
for i in range(len(adc)):
    adc[i] = round(adc[i] * 5 / 1023, 2)
print(adc)
المُخرَج
[0.0, 1.25, 2.5, 5.0]

رأينا في البند السابق أن v = v * 2 لا يغيّر القائمة. أما adc[i] = ... فيكتب في خانة القائمة نفسها.

مثال 6.10
الحالة الثانية: قائمتان متوازيتان. في قائمة جهود ثلاثة خطوط تغذية، وفي قائمة ثانية تياراتها، بالترتيب نفسه. المؤشّر نفسه يدل على الخط نفسه في القائمتين. نبني قائمة القدرات.
الحل:
volts = [12, 5, 3.3]
amps = [0.5, 0.2, 0.1]
power = []
for i in range(len(volts)):
    power.append(round(volts[i] * amps[i], 2))
print(power)
print("Total:", round(sum(power), 2))
المُخرَج
[6.0, 1.0, 0.33]
Total: 7.33
مثال 6.11
الحالة الثالثة: عنصر وجاره. إشارة رقمية مقيسة تقفز بين نحو 0 V ونحو 3.3 V. نبحث عن الحواف، أي الأماكن التي تتغيّر فيها العينة بأكثر من 1 V عن سابقتها.
الحل:
samples = [0.1, 0.2, 3.2, 3.3, 3.2, 0.2, 0.1]
for i in range(1, len(samples)):
    step = samples[i] - samples[i - 1]
    if abs(step) > 1:
        print("Edge at index", i)
المُخرَج
Edge at index 2
Edge at index 5

بدأنا من range(1, ...) لأن العنصر الأول ليس قبله جار. ولو بدأنا من 0 لقارنّا العينة الأولى بالعنصر [-1]، أي بالعينة الأخيرة، وهذا خطأ منطقي لا تصدر عنه أي رسالة.

الأمران break وcontinue من الفصل 4 يعملان في for أيضا: عندما نبحث عن أول عنصر يحقق شرطا، نكتب break فور أن نجده، ولا نكمل المرور.

خطأ شائع: هناك خطآن متكرران في السطر الأول من الحلقة. الأول range(len(v) + 1): دورة زائدة تصل بالمؤشّر إلى خارج القائمة. والثاني range(v): نسينا len، فتتلقى range قائمة بدل عدد. وكلاهما يوقف البرنامج.
ما نريدهالحلقة المناسبة
نقرأ العناصر فقط، واحدا بعد الآخرfor v in values:
نغيّر العناصر، أو نحتاج إلى قائمتين معا، أو إلى عنصر وجارهfor i in range(len(values)):
نكرر عددا معروفا من المراتfor k in range(n):
لا نعرف كم مرة سنكرر: حتى قيمة توقف أو حتى يتحقّق شرطwhile condition:
سؤال 6.6: ماذا يطبع البرنامج التالي؟
a = [2, 5, 1, 8]
t = 0
for i in range(1, len(a)):
    if a[i] > a[i - 1]:
        t += 1
print(t)
for i in range(0, len(a), 2):
    print(a[i])

6.9 المعامل in – אופרטור היחס in (in)

التعبير x in values يسأل: هل هذه القيمة عنصر في القائمة؟ ونتيجته قيمة بوليانية، ونقيضه x not in values. عرفنا هذا المعامل مع الجمل النصية، وهو يقارن هنا عنصرا كاملا. في المثال التالي القائمة هي سلسلة E12، أي القيم القياسية للمقاومات. العدد 47 والجملة النصية "47" ليسا القيمة نفسها، ولذلك جاء السطر الأخير False.

e12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
print(47 in e12)
print(50 in e12)
print(50 not in e12)
print("47" in e12)
المُخرَج
True
False
True
False
ملاحظة: الكلمة in لها دوران: في for v in values: هي جزء من الحلقة، وفي if v in values: هي سؤال جوابه True أو False.
مثال 6.12
في قائمة القطع التي استُعملت في لوحة، يتكرر بعضها. نبني قائمة بالقطع المختلفة، بلا تكرار.
الحل:
codes = ["R1", "C4", "R1", "D2", "C4"]
unique = []
for code in codes:
    if code not in unique:
        unique.append(code)
print(unique)
المُخرَج
['R1', 'C4', 'D2']

نضيف القطعة إلى القائمة الجديدة فقط إذا لم تكن فيها بعد. هذا النمط يتكرر كثيرا.

تنبيه: مع الجملة النصية يبحث in عن جزء منها، أما مع القائمة فيبحث عن عنصر مساو تماما: "V" in ["12V"] يعطي False. وهو يقارن بالمساواة التامة، ولذلك لا يصلح للأعداد العشرية المحسوبة، كما تعلمنا في البند 3.3: 0.3 in [0.1 + 0.2] يعطي False.
سؤال 6.7: ماذا يطبع البرنامج التالي؟
a = [1, 3, 5]
b = [3, 4, 5, 6]
for x in b:
    if x in a:
        print(x)
print(2 in a or 4 in b)
print([3] in a)

6.10 عمليّات الفئة list – עבודה עם פעולות של המחלקה

القائمة كائن من الفئة list، ولهذه الفئة عمليّات جاهزة نستدعيها بنقطة بعد اسم القائمة، كما استدعينا عمليّات الجملة النصية في الفصل 5. والفرق المهم عن الجملة النصية أن هذه العمليّات تغيّر القائمة نفسها، ولا تعطي قائمة جديدة.

الأمرعمله
a.append(x)يضيف العنصر x إلى النهاية
a.insert(i, x)يدخل العنصر x في المؤشّر i، ويزيح ما بعده
a.extend(b)يضيف كل عناصر b إلى النهاية
a.remove(x)يحذف أول عنصر يساوي x
a.count(x)يعطي عدد مرات ظهور x، ولا يغيّر القائمة
a.sort()يفرز القائمة تصاعديا في مكانها
a.clear()يفرغ القائمة
parts = ["R1", "C4", "D2", "C4"]
parts.append("LED")
print(parts)
parts.remove("C4")
print(parts)
print(parts.count("C4"))
parts.insert(1, "Q1")
print(parts)
parts.extend(["U1", "U2"])
print(parts)
parts.clear()
print(parts)
المُخرَج
['R1', 'C4', 'D2', 'C4', 'LED']
['R1', 'D2', 'C4', 'LED']
1
['R1', 'Q1', 'D2', 'C4', 'LED']
['R1', 'Q1', 'D2', 'C4', 'LED', 'U1', 'U2']
[]

حذفت remove أول ظهور فقط للقطعة C4، ولذلك بقي واحد، وهذا ما أكدته count. والعمليّة append تضيف عنصرا واحدا حتى لو كان قائمة، أما extend فتضيف عناصر القائمة واحدا واحدا.

العمليّة sort – הפעולה sort:

العمليّة sort تفرز الأعداد حسب قيمتها، والجمل النصية حسب الترتيب المعجمي (סדר מילוני)، أي حرفا بعد حرف كما في القاموس. وإذا كتبنا reverse=True كان الفرز تنازليا:

r = [470, 10, 220, 1000, 47]
r.sort()
print(r)
r.sort(reverse=True)
print(r)
names = ["diode", "LED", "resistor", "Capacitor"]
names.sort()
print(names)
codes = ["100", "47", "9"]
codes.sort()
print(codes)
المُخرَج
[10, 47, 220, 470, 1000]
[1000, 470, 220, 47, 10]
['Capacitor', 'LED', 'diode', 'resistor']
['100', '47', '9']

المقارنة بين الحروف تتم حسب رقم كل حرف في جدول الترميز، الذي تعطيه ord من الفصل 2. وفيه تسبق الحروف الكبيرة جميع الحروف الصغيرة، ولذلك جاءت LED قبل diode. والأرقام في جملة نصية هي حروف أيضا، ولذلك جاءت الجملة "100" قبل الجملة "9"، مع أن العدد 100 أكبر من 9.

خطأ شائع: العمليّة sort تفرز في مكانها ولا تعطي قيمة: ما تعيده هو None، أي «لا شيء». إذا كتبنا r = r.sort() فقدنا القائمة، وكذلك مع append. نكتب الأمر وحده في سطر:
r = [470, 10, 220]
r = r.sort()
print(r)
print(r[0])
المُخرَج
None
TypeError: 'NoneType' object is not subscriptable
مثال 6.13
خمس عينات من جهد ثابت، وإحداها أفسدها تشويش. متوسطها يبتعد عن الحقيقة، أما القيمة الوسطى (חציון)، وهي العنصر الأوسط بعد الفرز، فلا تتأثر. نحسب الاثنين، ونفرز نسخة كي نحافظ على ترتيب العينات الأصلي.
الحل:
samples = [2.5, 2.6, 9.9, 2.4, 2.5]
ordered = samples[:]
ordered.sort()
print(ordered)
print("Median:", ordered[len(ordered) // 2])
print("Average:", round(sum(samples) / len(samples), 2))
المُخرَج
[2.4, 2.5, 2.5, 2.6, 9.9]
Median: 2.5
Average: 3.98

هذا ما يفعله مرشّح القيمة الوسطى في معالجة الإشارات. ولو كتبنا ordered = samples لفرزنا القائمة الأصلية أيضا، كما رأينا في البند 6.4.

العمليّة remove – הפעולה remove:

خطأ شائع: حذف عنصر غير موجود يوقف البرنامج:
parts = ["R1", "C4"]
parts.remove("D2")
المُخرَج
ValueError: list.remove(x): x not in list

لذلك نفحص بالمعامل in قبل الحذف. ولحذف كل الظهورات نكرر الفحص بالحلقة while:

parts = ["R1", "C4", "R1", "D2", "R1"]
name = "Q7"
if name in parts:
    parts.remove(name)
else:
    print(name, "not found")
while "R1" in parts:
    parts.remove("R1")
print(parts)
المُخرَج
Q7 not found
['C4', 'D2']
خطأ شائع: لا نحذف عناصر من قائمة ونحن نمر عليها. عند حذف عنصر تنتقل العناصر التي بعده خطوة إلى الوراء، فتتخطى الحلقة العنصر التالي. في البرنامج التالي نريد أن نحذف كل الأصفار، ومع ذلك بقي واحد، ولم تظهر أي رسالة خطأ:
samples = [5, 0, 0, 7, 0, 3]
for s in samples:
    if s == 0:
        samples.remove(s)
print(samples)
المُخرَج
[5, 7, 0, 3]

الحل الأوضح هو أن نبني قائمة جديدة بالعناصر التي نريد أن نبقيها. وحل آخر أن نمر على نسخة ونحذف من الأصلية: for s in samples[:]:.

samples = [5, 0, 0, 7, 0, 3]
clean = []
for s in samples:
    if s != 0:
        clean.append(s)
print(clean)
المُخرَج
[5, 7, 3]
سؤال 6.8: ماذا يطبع البرنامج التالي؟
a = [8, 2, 6]
a.insert(0, 2)
a.append(4)
a.remove(2)
print(a)
a.sort()
print(a)
print(a.count(2))
a.extend([1, 1])
print(len(a))

6.11 قائمة من قوائم – רשימה של רשימות

عنصر القائمة قد يكون قائمة أيضا. القائمة من قوائم (רשימה של רשימות) هي الطريقة الطبيعية لتمثيل جدول: كل قائمة داخلية سطر، والمؤشّر الأول يختار السطر، والثاني يختار العمود.

012grid[0]102030grid[1]405060grid[1][2] = 60
grid[1] هو السطر كله، وgrid[1][2] هو عنصر واحد منه
grid = [[10, 20, 30],
        [40, 50, 60]]
print(grid[1])
print(grid[1][2])
print(len(grid), len(grid[0]))
grid[0][1] = 25
print(grid)
المُخرَج
[40, 50, 60]
60
2 3
[[10, 25, 30], [40, 50, 60]]

نقرأ grid[1][2] من اليسار إلى اليمين: grid[1] هو قائمة، ثم نأخذ منها العنصر [2]. وlen(grid) هو عدد الأسطر، وlen(grid[0]) هو عدد الأعمدة. ونكتب الجدول في البرنامج على عدة أسطر كي يشبه شكله الحقيقي، وبايثون تسمح بذلك داخل الأقواس.

للمرور على الجدول نكتب حلقة داخل حلقة. الحلقة الخارجية تمر على الأسطر، والداخلية على عناصر السطر الواحد، والأمر print() بعد الداخلية ينقلنا إلى سطر جديد:

grid = [[5, 1, 0],
        [2, 8, 4]]
for r in range(len(grid)):
    for c in range(len(grid[r])):
        print(grid[r][c], end=" ")
    print()
المُخرَج
5 1 0 
2 8 4 
مثال 6.14
علامات ثلاثة طلاب في ثلاث تجارب مختبر: كل سطر طالب، وكل عمود تجربة. نطبع متوسط كل طالب، ثم متوسط كل تجربة.
الحل:
lab = [[78, 85, 90],
       [60, 72, 64],
       [95, 88, 91]]
for row in lab:
    print(row, round(sum(row) / len(row), 1))
for c in range(len(lab[0])):
    total = 0
    for r in range(len(lab)):
        total += lab[r][c]
    print("Experiment", c, round(total / len(lab), 1))
المُخرَج
[78, 85, 90] 84.3
[60, 72, 64] 65.3
[95, 88, 91] 91.3
Experiment 0 77.7
Experiment 1 81.7
Experiment 2 81.7

متوسط السطر سهل، فالسطر قائمة جاهزة نعطيها إلى sum، ونمر عليه مباشرة بلا مؤشّرات. أما متوسط العمود فيحتاج إلى حلقتين، لأن عناصر العمود موزعة على الأسطر. لذلك تكون الحلقة الخارجية هنا على الأعمدة، والداخلية على الأسطر.

نبني جدولا بالطريقة نفسها: قائمة خارجية فارغة، وفي كل دورة نبني سطرا ونضيفه إليها:

rows = 2
cols = 3
grid = []
for r in range(rows):
    row = []
    for c in range(cols):
        row.append(r * 10 + c)
    grid.append(row)
print(grid)
المُخرَج
[[0, 1, 2], [10, 11, 12]]
خطأ شائع: هذه هي مشكلة الإسناد من البند 6.4 في ثوب جديد. الكتابة [[0] * 3] * 2 لا تنشئ سطرين، بل تكرر اسم السطر نفسه مرتين. نغيّر خانة واحدة فتتغيّر في «السطرين»:
grid = [[0] * 3] * 2
grid[0][0] = 7
print(grid)
المُخرَج
[[7, 0, 0], [7, 0, 0]]
سؤال 6.9: ماذا يطبع البرنامج التالي؟
m = [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]]
print(m[2][0], m[0][2])
d = 0
for i in range(len(m)):
    d += m[i][i]
print(d)
print(m[1][1:])

6.12 كتابة خوارزميات – כתיבת אלגוריתמים (שאינם מובנים בשפה)

الدوال max وmin والعمليّة sort تفعل كثيرا مما سنكتبه هنا. فلماذا نكتبه بأنفسنا؟ لأن هذه الخوارزميات هي أساس التفكير البرمجي، ولأن امتحان البجروت يطلبها، ولأننا كثيرا ما نحتاج إلى تعديل صغير لا توفره الأدوات الجاهزة، مثل مكان العنصر الأكبر لا قيمته فقط.

(أ) أكبر عنصر – מציאת האיבר הגדול ביותר:

الفكرة التي عرفناها في الفصل 4 هي القيمة القصوى (ערך קיצון): نفترض أن العنصر الأول هو الأكبر، ثم نمر على الباقي، وكل عنصر أكبر مما وجدنا يأخذ مكانه.

وكثيرا ما نحتاج إلى مكان العنصر لا إلى قيمته فقط، مثل رقم القناة ذات الجهد الأعلى. عندها نحفظ مؤشّره، وقيمته موجودة في القائمة:

volts = [12.1, 11.6, 12.9, 11.9]
pos = 0
for i in range(1, len(volts)):
    if volts[i] > volts[pos]:
        pos = i
print("Highest:", volts[pos], "at index", pos)
المُخرَج
Highest: 12.9 at index 2
خطأ شائع: نبدأ من عنصر من القائمة، لا من الصفر. إذا كانت كل القيم سالبة، مثل درجات حرارة في الشتاء، فإن الصفر يبقى هو «الأكبر» مع أنه ليس في القائمة أصلا:
temps = [-4, -9, -2]
top = 0
for t in temps:
    if t > top:
        top = t
print(top)
المُخرَج
0

الخوارزمية نفسها تعمل على الجمل النصية، فالمعاملان < و> يقارنان بينها حسب الترتيب المعجمي:

print("diode" < "resistor")
print("diode" < "diodes")
print("Zener" < "diode")
print("100" < "9")
print(100 < 9)
المُخرَج
True
True
True
True
False

(ب) أصغر عنصر – מציאת האיבר הקטן ביותר:

نقلب اتجاه المقارنة فقط: < بدل >. وعندما يتكرر الأصغر تبقى المقارنة < على الظهور الأول، ولو كتبنا <= لوصلنا إلى الأخير.

مثال 6.15
نجد القطعة الأولى والقطعة الأخيرة في الترتيب المعجمي، كما في فهرس مخزن. نفحص الاتجاهين في الدورة نفسها.
الحل:
parts = ["resistor", "diode", "capacitor", "zener"]
first = parts[0]
last = parts[0]
for name in parts:
    if name < first:
        first = name
    if name > last:
        last = name
print(first)
print(last)
المُخرَج
capacitor
zener

(ج) دمج قائمتين – מיזוג שני מבני נתונים:

الدمج (מיזוג) يعني أن نبني من قائمتين مفروزتين قائمة واحدة مفروزة. مثال ذلك سجلان لأحداث من جهازين، وكل سجل مرتب حسب الزمن. نمسك بمؤشّر في كل قائمة، ونقارن بين العنصرين اللذين يشيران إليهما. الأصغر ينتقل إلى الناتج، ويتقدم مؤشّره خطوة.

a = [3, 8, 15]
b = [1, 9, 11, 20]
merged = []
i = 0
j = 0
while i < len(a) and j < len(b):
    if a[i] <= b[j]:
        merged.append(a[i])
        i += 1
    else:
        merged.append(b[j])
        j += 1
merged += a[i:]
merged += b[j:]
print(merged)
المُخرَج
[1, 3, 8, 9, 11, 15, 20]

الحلقة تنتهي عندما تنفد إحدى القائمتين. ما بقي في الأخرى أكبر من كل ما مضى، ولذلك يُضاف كما هو. أحد القصّين a[i:] وb[j:] يكون فارغا حتما، فلا حاجة إلى فحص أيهما.

ملاحظة: يمكن أن نكتب c = a + b ثم c.sort()، ونحصل على النتيجة نفسها. لكن الدمج يمر على كل عنصر مرة واحدة، فهو أكثر نجاعة (יעילות) عندما تكون القائمتان مفروزتين.

(د-1) الفرز الفقاعي – מיון בועות:

الفكرة في الفرز الفقاعي (מיון בועות) بسيطة: نقارن كل عنصر بجاره، وإذا كانا بالترتيب الخاطئ نبدّل بينهما. بعد مرور واحد على القائمة يكون العنصر الأكبر قد وصل إلى نهايتها، كفقاعة تصعد إلى السطح. نتابع أولا مرورا واحدا:

j = 073915j = 137915j = 237915j = 337195nums37159
مرور واحد: الزوج الأصفر تبادل مكانيه، والزوج الأزرق بقي كما هو، والأخضر في مكانه النهائي

وهذا المرور في برنامج، على قائمة أقصر. التبديل بسطر واحد كما تعلمنا في البند 2.6، لأن الجهة اليمنى تُحسب كلها قبل الإسناد:

nums = [7, 3, 9, 1]
for j in range(3):
    if nums[j] > nums[j + 1]:
        nums[j], nums[j + 1] = nums[j + 1], nums[j]
print(nums)
متابعة مرور واحد
الأمرnumsjnums[j] > nums[j + 1]المُخرَج
nums = [7, 3, 9, 1][7, 3, 9, 1]
for j in range(3):0
if nums[j] > nums[j + 1]:True
nums[j], nums[j + 1] = nums[j + 1], nums[j][3, 7, 9, 1]
for j in range(3):1
if nums[j] > nums[j + 1]:False
for j in range(3):2
if nums[j] > nums[j + 1]:True
nums[j], nums[j + 1] = nums[j + 1], nums[j][3, 7, 1, 9]
for j in range(3):
print(nums)[3, 7, 1, 9]

العنصر الأكبر وصل إلى مكانه، لكن الباقي لم يُفرز بعد. لذلك نكرر المرور. في كل مرور جديد يدخل عنصر آخر إلى مكانه في النهاية، فنقصّر المدى بواحد. وقائمة من n عناصر تحتاج إلى n−1 مرور:

nums = [7, 3, 9, 1, 5]
n = len(nums)
for i in range(n - 1):
    for j in range(n - 1 - i):
        if nums[j] > nums[j + 1]:
            nums[j], nums[j + 1] = nums[j + 1], nums[j]
print(nums)
المُخرَج
[1, 3, 5, 7, 9]
nums73915i = 037159i = 131579i = 213579i = 313579
القائمة بعد كل مرور: المنطقة الخضراء المفروزة تكبر من النهاية

الشرط nums[j] > nums[j + 1] يحدد الاتجاه، ولفرز تنازلي نكتب <. ولأن المعامل > يعمل على الجمل النصية أيضا، فالبرنامج نفسه يفرزها حسب الترتيب المعجمي.

خطأ شائع: المدى في الحلقة الداخلية هو n - 1 - i. إذا كتبنا n فسيصل nums[j + 1] إلى خارج القائمة، لأن كل مقارنة تستعمل المؤشّر j + 1 أيضا.
سؤال 6.10: ماذا يطبع البرنامج التالي، وهو مرور واحد من الفرز الفقاعي؟ هل صارت القائمة مفروزة؟
v = [4, 2, 5, 1]
for j in range(3):
    if v[j] > v[j + 1]:
        v[j], v[j + 1] = v[j + 1], v[j]
print(v)

(د-2) فرز التبديل – מיון החלפה:

في فرز التبديل (מיון החלפה) نبني المنطقة المفروزة من البداية: نضع في الخانة الأولى أصغر عنصر. نقارن الخانة بكل خانة بعدها، وكلما وجدنا عنصرا أصغر بدّلنا بينهما. بعد ذلك ننتقل إلى الخانة الثانية، وهكذا.

nums = [7, 3, 9, 1, 5]
n = len(nums)
for i in range(n - 1):
    for j in range(i + 1, n):
        if nums[j] < nums[i]:
            nums[i], nums[j] = nums[j], nums[i]
print(nums)
المُخرَج
[1, 3, 5, 7, 9]
nums73915i = 017935i = 113975i = 213597i = 313579
القائمة بعد كل دورة خارجية: المنطقة الخضراء المفروزة تكبر من البداية

الفرق بين الفرزين: في الفقاعي نقارن كل عنصر بجاره ويصل الأكبر إلى النهاية، وفي التبديل نقارن خانة ثابتة بكل ما بعدها ويصل الأصغر إلى البداية. وكلاهما بحلقتين متداخلتين. فكم مقارنة يحتاج إليها الفرز؟ نعدّها:

for n in [5, 10, 100, 1000]:
    count = 0
    for i in range(n - 1):
        for j in range(n - 1 - i):
            count += 1
    print(n, count)
المُخرَج
5 10
10 45
100 4950
1000 499500

عشرة أضعاف العناصر تعني نحو مئة ضعف من المقارنات، لأن العدد يساوي n(n−1)/2. لذلك لا تعمل هذه الخوارزميات على ملايين العناصر، والعمليّة sort الجاهزة تستعمل خوارزمية أكثر نجاعة. نكتب الفرز بأنفسنا كي نفهم كيف يعمل، أما في برنامج عادي فنستعمل sort.

6.13 الصفّ الثابت: التهيئة والتحويل والطباعة – אתחול Tuple, הפעולה tuple, הדפסה (tuple)

الصفّ الثابت (Tuple): سلسلة مرتبة من العناصر مثل القائمة، لكنها لا تتغيّر بعد إنشائها: لا نضيف إليها ولا نحذف منها ولا نغيّر عنصرا فيها.

نكتب عناصره بين قوسين مستديرين. الصفّ الثابت مناسب لقيم تنتمي معا، مثل ساعة القياس وجهده، أو لبيانات لا يجوز أن تتغيّر، مثل إعدادات طرف في متحكّم.

reading = (8, 12.4)
pin = ("A0", "input", 5)
empty = ()
one = (5,)
print(reading, pin)
print(empty, one, len(pin))
print(type(reading))
المُخرَج
(8, 12.4) ('A0', 'input', 5)
() (5,) 3
<class 'tuple'>

الدالة tuple تحوّل إلى صفّ ثابت كما تحوّل list إلى قائمة. والأقواس ليست ضرورية عند الإنشاء: الفاصلة هي التي تصنع الصفّ الثابت. لقد التقينا به سابقا من دون أن نسميه: الدالة divmod في الفصل 2 تعطي صفّا ثابتا، والإسناد v, i = 12, 0.5 يبني واحدا ثم يفككه:

print(tuple([1, 2, 3]))
print(tuple("ADC"))
print(tuple(range(4)))
pair = 12, 0.5
print(pair)
print(divmod(17, 5))
المُخرَج
(1, 2, 3)
('A', 'D', 'C')
(0, 1, 2, 3)
(12, 0.5)
(3, 2)
خطأ شائع: محاولة تغيير الصفّ الثابت توقف البرنامج، وهذا هو الغرض منه: يحمي القيم من تغيير بالخطأ. وليس له عمليّة append أيضا:
reading = (8, 12.4)
reading[1] = 12.0
المُخرَج
TypeError: 'tuple' object does not support item assignment
مثال 6.16
تبيّن أن قراءة في سجل الساعة 8 خاطئة، ويجب تصحيحها. لا نستطيع تغيير الصفّ الثابت، فنحوّله إلى قائمة، نغيّرها، ونبني صفّا ثابتا جديدا.
الحل:
reading = (8, 12.4)
temp = list(reading)
temp[1] = 12.0
reading = tuple(temp)
print(reading)
المُخرَج
(8, 12.0)
سؤال 6.11: ما نوع كل قيمة:
(أ) (5)
(ب) (5,)
(ج) 5؟ ونفسّر لماذا تختلف الأولى عن الثانية.

6.14 البنية التسلسلية للصفّ الثابت: المؤشّر والقصّ – המבנה הסידורי של Tuple, אינדקס וחיתוך

المؤشّر والقصّ يعملان كما في القائمة تماما، موجبين وسالبين، والقصّ يعطي صفّا ثابتا جديدا. والمؤشّر خارج المجال يوقف البرنامج هنا أيضا. ويمكن أن نفكك الصفّ الثابت إلى متغيّرات في أمر واحد، بشرط أن يساوي عددها عدد عناصره:

rec = (14, 12.6, "OK")
print(rec[0], rec[-1])
print(rec[1:])
hour, volts, status = rec
print(hour, volts, status)
المُخرَج
14 OK
(12.6, 'OK')
14 12.6 OK
مثال 6.17
أسماء ألوان المقاومات لا تتغيّر أبدا، فهي مرشحة لصفّ ثابت. نكتب مقاومة بالأرقام 4 و7 والمضاعف 2: نطبع ألوان أشرطتها وقيمتها.
الحل:
colors = ("black", "brown", "red", "orange", "yellow",
          "green", "blue", "violet", "gray", "white")
first = 4
second = 7
power = 2
print(colors[first], colors[second], colors[power])
print((first * 10 + second) * 10 ** power, "ohm")
المُخرَج
yellow violet red
4700 ohm

المؤشّر هو الرقم نفسه، ولذلك نترجم من رقم إلى لون بلا شروط.

سؤال 6.12: ماذا يطبع البرنامج التالي؟
t = (2, 4, 6, 8, 10)
print(t[1], t[-1])
print(t[1:3])
print(t[::2])
a, b = t[:2]
print(a + b)

6.15 الحلقة for على الصفّ الثابت – לולאת for העוברת על איברי Tuple

مثال 6.18
الحلقة for تعمل هنا كما في القائمة، على العناصر وعلى المؤشّرات. وأقوى ما في الصفّ الثابت أن نضعه داخل قائمة: كل صفّ هو سجل لقراءة واحدة لا تتغيّر، والقائمة هي السجل كله الذي يكبر مع الوقت. نطبع سجل قراءات، ونفكك كل صفّ مباشرة في سطر الحلقة، وهذا أوضح من كتابة المؤشّرات.
الحل:
log = [(8, 12.4), (10, 12.7), (12, 13.1)]
print(log[1], log[1][0])
for hour, volts in log:
    print("Hour", hour, ":", volts, "V")
المُخرَج
(10, 12.7) 10
Hour 8 : 12.4 V
Hour 10 : 12.7 V
Hour 12 : 13.1 V
سؤال 6.13: ماذا يطبع البرنامج التالي؟
log = [(1, 5), (2, 9), (3, 4)]
s = 0
for a, b in log:
    if a != 2:
        s += b
print(s)

6.16 العمليّتان count و index – עבודה עם פעולות המחלקה: count, index

لأن الصفّ الثابت لا يتغيّر، فليس له إلا عمليّتان، وكلتاهما تسأل ولا تغيّر. العمليّة count تعطي عدد مرات ظهور القيمة، والعمليّة index تعطي مؤشّر أول ظهور لها، ويمكن أن نحدد لها من أين تبدأ البحث.

bits = (1, 0, 1, 1, 0, 1)
print(bits.count(1))
print(bits.index(0), bits.index(0, 2))
print(bits.count(1) % 2)
المُخرَج
4
1 4
0

السطر الأخير يحسب خانة الزوجية (סיבית זוגיות): باقي قسمة عدد الخانات التي قيمتها 1 على 2. سنعود إلى هذه الخانة في الاتصال التسلسلي في الفصل 10. والعمليّة index موجودة في القائمة أيضا، وتعمل بالطريقة نفسها.

مثال 6.19
البحث عن قيمة غير موجودة بالعمليّة index يوقف البرنامج. لذلك نفحص بالمعامل in أولا. نبحث عن خط تغذية بين خطوط اللوحة.
الحل:
rails = (3.3, 5, 12, 5)
wanted = 5
if wanted in rails:
    print(rails.index(wanted), rails.count(wanted))
else:
    print("No such rail")
المُخرَج
1 2
سؤال 6.14: ماذا يطبع البرنامج التالي؟
days = (0, 2, 2, 5, 2, 0)
print(days.count(2))
print(days.index(2))
print(days.index(0, 1))
▶تجربة حية — مختبر بنى المعطياتنبني قائمة، نمر عليها بالحلقة for خطوة بعد خطوة، ونرى المؤشّر والعناصر تتغيّر أمامنا. لا يوجد في المختبر إدخال من المستخدم، ولذلك نكتب القيم مباشرة في القائمة بدل input.
⚠
المختبر غير متاح الآن. المادة في هذا الفصل كاملة من دونه — نواصل القراءة، ونعود إلى التجربة لاحقا أو نفتحها مباشرة من الرابط الذي تحت الإطار.
https://fahed-lab.com/electronics/python_basics#data
نفتح المختبر في صفحة مستقلة ↗ fahed-lab.com/electronics/python_basics#data
مشروع الكتاب: محطة القياس · المرحلة 6 — المحطة تتذكّر

في الفصل 4 قرأت المحطة سلسلة قياسات، لكنها كانت تنسى كل قراءة بعد الأخرى. الآن تتذكر: كل قياس يصبح صفّا ثابتا (hour, volts) يُحفظ في قائمة السجل. قائمة عدّادات تصنّف القياسات إلى ثلاثة مجالات للجهد: منخفض تحت 11.8 V، وسليم حتى 12.6 V، وعال فوق ذلك. وفي النهاية تبحث المحطة عن ساعة الجهد الأعلى، وتفرز القراءات.

log = []
counts = [0, 0, 0]
hour = int(input("Hour (-1 to stop): "))
while hour != -1:
    volts = float(input("Battery V: "))
    log.append((hour, volts))
    if volts < 11.8:
        counts[0] += 1
    elif volts <= 12.6:
        counts[1] += 1
    else:
        counts[2] += 1
    hour = int(input("Hour (-1 to stop): "))
print(log)
print("Low, OK, High:", counts)
المُخرَج
Hour (-1 to stop): 8
Battery V: 12.4
Hour (-1 to stop): 12
Battery V: 13.1
Hour (-1 to stop): 18
Battery V: 11.6
Hour (-1 to stop): -1
[(8, 12.4), (12, 13.1), (18, 11.6)]
Low, OK, High: [1, 1, 1]

في الجزء الثاني تعمل المحطة على السجل الذي بنته. نضعه هنا مباشرة في البرنامج كي نجرّب هذا الجزء وحده، وفي البرنامج الكامل يأتي مباشرة بعد الحلقة:

log = [(8, 12.4), (12, 13.1), (18, 11.6)]
best = log[0]
readings = []
for rec in log:
    readings.append(rec[1])
    if rec[1] > best[1]:
        best = rec
print("Highest:", best[1], "V at hour", best[0])
readings.sort()
print("Sorted:", readings)
المُخرَج
Highest: 13.1 V at hour 12
Sorted: [11.6, 12.4, 13.1]

لم نستعمل max هنا، لأننا نريد السجل كله لا القيمة وحدها: الساعة مربوطة بالجهد في الصفّ نفسه. نجرّب بأنفسنا: نضيف إلى البرنامج طباعة الساعات التي كان الجهد فيها منخفضا. وماذا يحدث إذا أدخلنا -1 في أول سؤال؟ ولماذا؟

أهمّ أفكار الفصل:

ملخّص أوامر الفصل:

الأمرعمله
a = [1, 2] a = [] a = list(x)تهيئة قائمة
a[i] a[-1] a[i][j]الوصول إلى عنصر
a[start:stop:step] b = a[:]القصّ والنسخ
a + b a += b [0] * nالوصل والتكرار
range(start, stop, step)سلسلة أعداد صحيحة
for v in a:المرور على العناصر
for i in range(len(a)):المرور على المؤشّرات
x in a x not in aهل القيمة عنصر في القائمة؟
a.append(x) a.insert(i, x) a.extend(b)إضافة عناصر
a.remove(x) a.clear()حذف عناصر
a.count(x) a.sort()العدّ والفرز
t = (1, 2) t = (5,) t = tuple(x)تهيئة صفّ ثابت
t.count(x) t.index(x)عمليّتا الصفّ الثابت

قاموس المصطلحات – מילון מונחים:

بالعربيةبالعبرية (كما تظهر في الامتحان)English
بنية المعطياتמבנה נתוניםData structure
القائمةרשימהList
العنصرאיברElement
المؤشّرאינדקסIndex
القصّחיתוךSlicing
قائمة عدّاداتרשימת מוניםCounter list
قائمة مُجمِّعاتרשימת צובריםAccumulator list
القائمة الفارغةרשימה ריקהEmpty list
قائمة من قوائمרשימה של רשימותList of lists
قيمة قصوىערך קיצוןExtreme value
الترتيب المعجميסדר מילוניLexicographic order
الدمجמיזוגMerge
الفرز الفقاعيמיון בועותBubble sort
فرز التبديلמיון החלפהExchange sort
القيمة الوسطىחציוןMedian
الصفّ الثابتTupleTuple
خانة الزوجيةסיבית זוגיותParity bit
الإجابات النهائية لأسئلة الفصل
6.1 (أ) نعم، بمُجمِّع. (ب) لا، لأن المتوسط لا يُعرف إلا في النهاية، وعندها تكون القراءات قد ضاعت. (ج) نعم، بمتغيّر يحفظ القيمة القصوى.
6.2 [2, 0, 3, 1]
6.3 [7, 8, 3] , [7, 8, 3] , [1, 2, 9]
6.4 [1, 2, 4, 5] , [1, 2, 3] , [7, 7, 8]
6.5 0 , 7 , 7 , 16
6.6 2 , 2 , 1
6.7 3 , 5 , True , False
6.8 [8, 2, 6, 4] , [2, 4, 6, 8] , 1 , 6
6.9 7 3 , 15 , [5, 6]
6.10 [2, 4, 1, 5]. لا: العنصر الأكبر فقط وصل إلى مكانه، ونحتاج إلى مرور آخر على الأقل.
6.11 <class 'int'> , <class 'tuple'> , <class 'int'>. القوسان وحدهما حسابيان، أما الفاصلة فهي التي تصنع الصفّ الثابت.
6.12 4 10 , (4, 6) , (2, 6, 10) , 6
6.13 9
6.14 3 , 1 , 5

أسئلة للتمرين:

المستوى الأول — أساسي
1.
ماذا يطبع البرنامج؟
v = [4, 8, 15, 16, 23, 42]
print(v[2], v[-2])
print(v[1:3])
print(v[::3])
print(len(v[2:]))
الإجابات: 15 23 , [8, 15] , [4, 16] , 4
2.
نكتب برنامجا يقرأ أربعة تيارات إلى قائمة، بحلقة for، ويطبع مجموعها وأكبرها.
الإجابات:
currents = []
for k in range(4):
    currents.append(float(input("Current: ")))
print("Total:", sum(currents))
print("Largest:", max(currents))
للقيم 1.5, 0.5, 2.25, 0.75 يطبع: Total: 5.0 , Largest: 2.25
3.
نجد الخطأين في هذا البرنامج ونصلحهما.
volts = [12, 5, 3]
for i in range(len(volts) + 1):
    print(volts[i])
volts = volts.sort()
print(volts[0])
الإجابات: الحلقة تصل إلى مؤشّر غير موجود، والعمليّة sort تعيد None. والبرنامج الصحيح:
volts = [12, 5, 3]
for i in range(len(volts)):
    print(volts[i])
volts.sort()
print(volts[0])
وهو يطبع: 12 , 5 , 3 , 3
4.
الصفّ الثابت هو t = (1, 2, 3). أي الأوامر التالية يوقف البرنامج؟
(أ) print(t[0])
(ب) t[0] = 5
(ج) print(t.count(2))
(د) t.append(4)
الإجابات: (ب) و(د): لا يمكن تغيير الصفّ الثابت، وليس له عمليّة append.
المستوى الثاني — متوسط
5.
عينات من حسّاس جهد، من 0 حتى أقل من 5 V. نكتب برنامجا يعدّ كم عينة وقعت في كل مجال بعرض 1 V: من 0 حتى ما قبل 1، وهكذا. الجزء الصحيح من العينة هو المؤشّر.
الإجابات:
samples = [0.4, 1.2, 1.9, 3.3, 4.8, 1.1, 0.7, 3.9]
counts = [0, 0, 0, 0, 0]
for v in samples:
    counts[int(v)] += 1
print(counts)
يطبع: [2, 3, 0, 2, 1]
6.
نكتب برنامجا ينعّم إشارة بمتوسط متحرك: كل عنصر في القائمة الجديدة هو متوسط ثلاث عينات متتالية. نستعمل القصّ.
الإجابات:
samples = [2, 4, 9, 4, 2, 6]
smooth = []
for i in range(len(samples) - 2):
    window = samples[i:i + 3]
    smooth.append(round(sum(window) / 3, 2))
print(smooth)
يطبع: [5.0, 5.67, 5.0, 4.0]
7.
في طيف إشارة، القمة هي مستوى أعلى من جاريه. نكتب برنامجا يطبع مؤشّر كل قمة ومستواها.
الإجابات:
levels = [2, 7, 3, 4, 9, 1, 5, 2]
for i in range(1, len(levels) - 1):
    if levels[i - 1] < levels[i] > levels[i + 1]:
        print("Peak at index", i, "level", levels[i])
يطبع: Peak at index 1 level 7 , Peak at index 4 level 9 , Peak at index 6 level 5
8.
نبني جدول متابعة للبرنامج التالي.
a = [6, 3, 8]
b = []
for x in a:
    if x > 4:
        b.append(x - 4)
a[0] = len(b)
print(a, b)
الإجابات:
الأمرabxx > 4المُخرَج
a = [6, 3, 8][6, 3, 8]
b = [][]
for x in a:6
if x > 4:True
b.append(x - 4)[2]
for x in a:3
if x > 4:False
for x in a:8
if x > 4:True
b.append(x - 4)[2, 4]
for x in a:
a[0] = len(b)[2, 3, 8]
print(a, b)[2, 3, 8] [2, 4]
المستوى الثالث — تحدٍّ
9.
نكتب فرزا فقاعيا تنازليا يتوقف عندما يمر مرورا كاملا بلا أي تبديل، ويطبع عدد المرورات. نستعمل متغيّرا بوليانيا.
الإجابات:
nums = [3, 8, 2, 9, 4, 7]
n = len(nums)
passes = 0
swapped = True
while swapped:
    swapped = False
    passes += 1
    for j in range(n - 1):
        if nums[j] < nums[j + 1]:
            nums[j], nums[j + 1] = nums[j + 1], nums[j]
            swapped = True
print(nums)
print("Passes:", passes)
يطبع: [9, 8, 7, 4, 3, 2] , Passes: 4
10.
جدول فيه ثلاث قنوات، وفي كل سطر أربع عينات. نكتب برنامجا يطبع لكل قناة متوسطها وأعلى عينة فيها، ثم رقم القناة ذات المتوسط الأعلى. نلاحظ أنها ليست صاحبة أعلى عينة.
الإجابات:
data = [[1.2, 1.4, 1.1, 1.3],
        [3.0, 3.3, 3.1, 3.2],
        [0.5, 4.9, 0.4, 0.6]]
best = 0
top = sum(data[0]) / len(data[0])
for ch in range(len(data)):
    average = sum(data[ch]) / len(data[ch])
    print("Channel", ch, round(average, 2), max(data[ch]))
    if average > top:
        top = average
        best = ch
print("Highest average: channel", best)
يطبع: Channel 0 1.25 1.4 , Channel 1 3.15 3.3 , Channel 2 1.6 4.9 , Highest average: channel 1
11.
الخانات الثنائية لبايت محفوظة في صفّ ثابت، من الخانة الأعلى إلى الأدنى. نكتب برنامجا يحسب قيمة البايت، وخانة الزوجية.
الإجابات:
bits = (1, 0, 1, 1, 0, 0, 1, 0)
value = 0
for b in bits:
    value = value * 2 + b
print(value)
print("Parity bit:", bits.count(1) % 2)
يطبع: 178 , Parity bit: 0
12.
سجل إزاحة دائري: نكتب برنامجا يقرأ عدد الخطوات، ويزيح القائمة إلى اليمين. ما يخرج من النهاية يعود إلى البداية. نستعمل القصّ.
الإجابات:
reg = [1, 0, 0, 1, 1, 0]
k = int(input("Shift: "))
k = k % len(reg)
reg = reg[-k:] + reg[:-k]
print(reg)
للمُدخَل 2 يطبع: [1, 0, 1, 0, 0, 1]

שאלות בגרות – أسئلة بجروت للفصل 6

السؤالان التاليان من امتحان البجروت يعتمدان على هذا الفصل وعلى الفصل 4، ونحلهما كاملين كبرنامج عادي، لأننا لم نتعلم كتابة العمليّات بعد. يظهر السؤالان بالعبرية كما في الامتحان، إذ لا يوجد لهما نص عربي رسمي في هاتين السنتين.
بجروت 2026 السؤال 11 — التصويت لخمسين أغنية
بجروت 2026
البندالمطلوبالإجابة
(أ)نقرأ في حلقة رقم الأغنية حتى يُدخَل رقم خارج المجال، ونعدّ الأصوات في قائمة عدّادات.في الأسفل
(ب)نطبع رقم الأغنية التي نالت أكثر الأصوات، وأي واحدة منها عند التساوي.في الأسفل
(ج)نحسب متوسط الأصوات، ونطبع أرقام الأغاني التي نالت أقل منه.في الأسفل
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2026

(أ) الأغاني مرقّمة من 1 إلى 50 والمؤشّرات من 0 إلى 49، ولذلك يقع عدّاد الأغنية في الخانة song - 1. وشرط الحلقة هو المجال نفسه، فأي رقم خارجه ينهيها. (ب) نبحث عن مؤشّر الأكبر كما في البند 6.12، ونطبع المؤشّر زائد 1. (ج) نحسب المتوسط من مجموع الأصوات، ثم نمر على القائمة مرة أخرى. هذا بالضبط ما لا يمكن عمله من دون قائمة.

songs = 50
votes = [0] * songs
song = int(input("Song number: "))
while 1 <= song <= songs:
    votes[song - 1] += 1
    song = int(input("Song number: "))
best = 0
for i in range(1, songs):
    if votes[i] > votes[best]:
        best = i
print("Most votes: song", best + 1)
average = sum(votes) / songs
print("Average:", average)
print("Below average:")
for i in range(songs):
    if votes[i] < average:
        print(i + 1, end=" ")

خمسون أغنية تعني مُخرَجا طويلا لا يمكن فحصه بالعين. لذلك نغيّر السطر الأول مؤقتا إلى songs = 5، وندخل 2, 3, 2, 1, 2, 3, 5, 4, 2, 3, 6. الرقم 6 خارج المجال فينهي القراءة. يطبع البرنامج: Most votes: song 2 , Average: 2.0 , Below average: , 1 4 5

وعند التساوي تختار المقارنة > الأغنية الأولى، لأن الثانية ليست أكبر منها، والسؤال يقبل أي واحدة منهما:

متابعة البند (ب) لأربع أغنيات، اثنتان منها متساويتان
الأمرibestvotes[i] > votes[best]المُخرَج
votes = [3, 7, 7, 1]
best = 00
for i in range(1, 4):1
if votes[i] > votes[best]:True
best = i1
for i in range(1, 4):2
if votes[i] > votes[best]:False
for i in range(1, 4):3
if votes[i] > votes[best]:False
for i in range(1, 4):
print("Most votes: song", best + 1)Most votes: song 2
بجروت 2025 السؤال 11 — شركة التوصيل
بجروت 2025
البندالمطلوبالإجابة
القراءةنقرأ في حلقة رقم عامل التوصيل والتقييم والإكرامية، ونتوقف عند رقم غير قانوني.في الأسفل
الإكرامياتنطبع مجموع ما ناله عاملا التوصيل من إكراميات.في الأسفل
الأفضلنطبع رقم عامل التوصيل صاحب التقييم الأعلى.في الأسفل
العلاوةنطبع 20% من إكراميات عامل التوصيل الأفضل.في الأسفل
الحل الكامل بالعربية: fahed-lab.com/exams/electronics-815381/815381/solutions/solution-2025

لكل عامل توصيل قيمتان تتراكمان، فنبني قائمتي مُجمِّعات متوازيتين، والمؤشّر courier - 1 يدل على العامل نفسه في القائمتين. والشرط courier in [1, 2] يعبّر عن نهاية القراءة بدل شرطين مربوطين بالمعامل or.

tips = [0, 0]
ratings = [0, 0]
courier = int(input("Courier: "))
while courier in [1, 2]:
    ratings[courier - 1] += int(input("Rating (1-5): "))
    tips[courier - 1] += float(input("Tip: "))
    courier = int(input("Courier: "))
print("Total tips:", sum(tips))
if ratings[0] >= ratings[1]:
    best = 0
else:
    best = 1
print("Best courier:", best + 1)
print("Bonus:", tips[best] * 20 / 100)
المُخرَج
Courier: 1
Rating (1-5): 5
Tip: 20
Courier: 2
Rating (1-5): 4
Tip: 30
Courier: 1
Rating (1-5): 2
Tip: 10
Courier: 0
Total tips: 60.0
Best courier: 1
Bonus: 6.0
ملاحظة: السؤال لا يقول كيف يُحسب «التقييم الأعلى»، ولا ماذا نفعل عند التساوي. اعتبرنا التقييم مجموع ما ناله كل عامل، وعند التساوي يفوز العامل 1. في الامتحان نكتب مثل هذا الافتراض صراحة بجانب الحل. وننتبه: في مثال الإدخال أعلاه كان المتوسط سيعطي الفوز للعامل 2 لا للعامل 1. ومن يفضّل المتوسط يحتاج إلى قائمة عدّادات ثالثة، وإلى فحص ألا يكون عدد توصيلات العامل صفرا.