6  التسلسل

كثيرًا ما نحتاج للتعامل مع الأشياء في مجموعة. وذلك مثلاً لترتيب المجموعة أو عكسها أو ربطها مع مجموعة أخرى، أو البحث فيها، أو تصفيتها، أو تحويلها جميعًا بنفس الطريقة، أو استخلاص قيمة منها، …إلخ من العمليات التي تعمل على جميع عناصر المجموعة.

الجمع (Collection)

الجمع (Collection) ضد المفرد (Atomic). وهو ما يقبل الإجراءات التالية:

ملاحظة

نرمز للمفرد بـx ولما يدل على الجمع بـc.

  • العضوية: x not in c
  • العد: len(c)
  • التكرار: for x in c

انظر خريطة الجموع شكل 1 حيث يتبين أنه مكون من ثلاثة:

  • الحاوي: Container (لقبوله العضوية)
  • ذو الحجم: Sized (لقبوله العد)
  • المكرر: Iterable (لقبوله التكرار)

ويتفرع منه ثلاثة:

  • التسلسل: Sequence
  • المجموعة: Set
  • الدالة: Mapping

فالأول موضوع هذا الباب، والآخران في الباب التالي إن شاء الله.

التسلسل (Sequence)

التسلسل (Sequence) جمع مرتب من الأشياء.

  • جمع: يعني قبوله الإجراءات الثلاثة السابق ذكرها (العضوية والعد والتكرار).
  • مرتب: يعني أن لكل عنصرٍ موضعًا بالنسبة لبدايته.

ويبدأ ترقيم المواضع بالنسبة لبداية التسلسل لذلك نجعل للعنصر الأوَّل الموضِع 0، إذ نسبةُ ذلك لبداية التسلسل. ويكون موضع الثاني 1 بالنسبة لبداية التسلسل، وللثالث 2، وهكذا إلخ.

ومن أمثلة المجموعات المرتبة:

  • قائمة الرسائل، إذ هي مرتبة بالوقت واحدة تتلو الأخرى
  • مجموعة الحروف في اللغة العربية، إذ تبدأ بالألف وتنتهي بالياء وما بينهما كلٌّ له ما قبله وما بعده
  • قائمة الانتظار التي تعطي الأولويَّة لمن يأتي أوَّلاً للدخول على الطبيب

والمشترك في هذه الأمثلة الثلاثة: أن العناصر لها موضِعٌ بالنسبة لبعضها (مرتَّبة).

والأنواع الأربعة التي من جنس التسلسل هي:

  1. القائمة (list) ويُعبَّرُ عنه بالقوسين المربعين [].
  2. الصف (tuple) ويُعبَّرُ عنه بالقوسين المنحنيين ().
  3. المجال (range) ويُعبَّرُ عنه بالإجراء المنشئ range().
  4. النص (str) ويُعبَّرُ عنه بالتنصيص المفرد '' أو المزدوج ""
flowchart TD
    Collection[<b>الجمع</b> <br> <code>Collection</code>]
    Collection --> Sequence[<b>التسلسل</b> <br> <code>Sequence</code>]
    Sequence --> MutableSequence[<b>التسلسل المتغير</b> <br> <code>MutableSequence</code>]
    MutableSequence --> list[<b>القائمة</b> <br> <code>list</code>]
    Sequence --> tuple[<b>الصف</b> <br> <code>tuple</code>]
    Sequence --> range[<b>المجال</b> <br> <code>range</code>]
    Sequence --> str[<b>النص</b> <br> <code>str</code>]
شكل 6.1: شجرة أنواع التسلسل

وراجع خريطة المجموعات: شكل 1

فهذه الأربعة تقبل الإجراءات التالية (المتغير s هو التسلسل هنا):

  • الإشارة:
    • بالموضع: s[i]
    • بالقطعة: s[i:j]
    • بالقطعة مع خطوة: s[i:j:k]
  • معرفة موضع شيء (إن وجد): s.index(x)
  • عد تكرارات شيء: s.count(x)
  • البحث عن الأصغر والأكبر: min(s) و max(s)

وتقبل من إجراءات الإنشاء:

  • الدمج: s1 + s2
  • التكرار: s * n

أما تخصيص حرف + للدمج (لا للجمع) ، وحرف * للتكرار (لا للضرب)؛ فسيأتي معنا -إن شاء الله- في فصل تعريف الإجراءات المخصوصة في باب الأنواع.

الصف (tuple)

الصف تسلسل جامد.

فالجامد هو ما لا يقبل التغيير بعد إنشائه.

الإنشاء

يكون إنشاء الصف بالقوسين المنحنيين () على النحو التالي:

  • الفرد: (x,) وهو صف بعنصر واحد
  • الزوج: (x, y) وهو صف بعنصرين
  • الثلاثي: (x, y, z) وهو صف بثلاثة عناصر
  • …إلخ.

ولا يشترط تجانس العناصر؛ بل يجوز أن تكون أنواعها مختلفة:

s = (10, 20, 'hello', True, (300, 400))
print(s)
(10, 20, 'hello', True, (300, 400))

وقد يتألف الإنشاء بالتكرار بعلامة *:

s = (10, 20) * 3
print(s)
(10, 20, 10, 20, 10, 20)

أو الدمج، بعلامة +:

s = (10, 20) + (30, 40)
print(s)
(10, 20, 30, 40)

نستعرض هنا العضوية والعد والتكرار:

s = (100, 200, 300)

assert 100 in s
assert 400 not in s
assert len(s) == 3

for x in s:
    print(x)
100
200
300

الإشارة

تستعمل الإشارة الموضعية لقراءة عنصر من التسلسل.

s = (10, 20, 30, 40, 50)
assert s[0] == 10
assert s[-1] == 50
assert s[len(s) // 2] == 30
 0    1    2    3    4    5     
 +----+----+----+----+----+
 | 10 | 20 | 30 | 40 | 50 |
 +----+----+----+----+----+
-5   -4   -3   -2   -1

ويجب أن يكون المؤشر رقمًا صحيحًا لا يتجاوز نطاق التسلسل.

فهذا يفشل لأن المؤشر ليس رقمًا صحيحًا:

s = (10, 20, 30, 40, 50)
s['3']
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[149], line 2
      1 s = (10, 20, 30, 40, 50)
----> 2 s['3']

TypeError: tuple indices must be integers or slices, not str

وكذلك هذا لأنه يتجاوز نطاق التسلسل (0 - 4):

s = (10, 20, 30, 40, 50)
s[5]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[150], line 2
      1 s = (10, 20, 30, 40, 50)
----> 2 s[5]

IndexError: tuple index out of range

شكل الإشارة بالقطعة (slice) على نحو: s[start : end : step]. والقيم الابتدائية عند الإغفال هي: s[0:len(s):1].

 0    1    2    3    4    5     
 +----+----+----+----+----+
 | 10 | 20 | 30 | 40 | 50 |
 +----+----+----+----+----+
-5   -4   -3   -2   -1
s = (10, 20, 30, 40, 50)
assert s[1:3] == (20, 30)
assert s[::2] == (10, 30, 50)
assert s[::-1] == (50, 40, 30, 20, 10)
assert s[1:4:2] == (20, 40) == s[-4:-1:2]
assert s[1:4:2] == s[slice(1,4,2)] == (20, 40)

لاحظ استعمال الإجراء المنشئ slice() في الإشارة بالقطعة، وقد جعلت بايثون علامة : بديلاً عنه.

وإذا جعلته صفًّا من صفوف؛ صار مصفوفة:

matrix = (
    (10, 20, 30),
    (40, 50, 60),
    (70, 80, 90)
)

assert matrix[0] == (10, 20, 30)
assert matrix[-1] == (70, 80, 90)

وتستعمل الإشارة مرتين للإشارة إلى العدد، مثلاً للإشارة إلى العنصر الثاني في الصف الثاني:

assert matrix[1][1] == 50
 0              1              2              3
 +--------------+--------------+--------------+
 | (10, 20, 30) | (40, 50, 60) | (70, 80, 90) |
 +--------------+--------------+--------------+
-3             -2             -1

الإشارة لعناصر الصف الواحد:

 0    1    2    3
 +----+----+----+
 | 10 | 20 | 30 |
 +----+----+----+
-3   -2   -1
 0    1    2    3 
 +----+----+----+
 | 40 | 50 | 60 |
 +----+----+----+
-3   -2   -1
 0    1    2    3
 +----+----+----+
 | 70 | 80 | 90 |
 +----+----+----+
-3   -2   -1

عناصر نصوص:

ss = ("Apple", "Banana", "Orange", "Lemon")
assert ss[1] == "Banana"
assert ss[-1][0] == "L"
 0       1        2        3       4
 +-------+--------+--------+-------+
 | Apple | Banana | Orange | Lemon |
 +-------+--------+--------+-------+
-4      -3       -2       -1

الإشارة لصف الأحرف في النص الواحد:

 0   1   2   3   4   5
 +---+---+---+---+---+
 | L | e | m | o | n |
 +---+---+---+---+---+
-5  -4  -3  -2  -1

وسيأتي التفصيل في باب النص.

البحث

البحث عن موضع العنصر (s.index(x)) وعد تكراره (s.count(x)):

s = ('Python', 'Python', 'Go')
assert s.index('Go') == 2
assert s.count('Python') == 2

البحث عن الأصغر والأكبر:

s = (30, 20, 40, 10, 50)
assert s.index(min(s)) == 3
assert s.index(max(s)) == 4

القائمة (list)

القائمة (list) تسلسل متغير.

انظر MutableSequence في خريطة المجموعات: شكل 1.

التغير

فالأنواع على قسمين من حيث قبول التغير بعد الإنشاء:

  • متغير (Mutable: قابل للتغيير): يعني قبوله الإضافة والحذف والتعديل على عناصرها بعد الإنشاء.
  • جامد (Immutable: غير قابل للتغيير): لا يقبل التغير. ومن جهة كونها عوامل للإجراء؛ فإن الجامد لا يقبل أن يكون محل عمل الإجراء.
ملاحظة

التغير والجمود مفهومان يتكرران كثيرًا في البرمجة. للمزيد راجع: بناء البرمجيات: الفصل التاسع، الجمود (MIT-6.005)

التغير هي الخاصية التي تختلف فيها القائمة عن قسيماتها التسلسلية (الصف والمجال والنص). ومعناه قبولها الإجراءات التالية (نستعمل في المثال حرف l للقائمة):

  • الاستبدال:
    • لموضع: l[i] = x
    • لقطعة: l[i:j] = t
    • لقطعة بخطوة: l[i:j:k] = t
  • الحذف:
    • لموضع: del l[i]
    • لقطعة: del l[i:j]
    • لقطعة بخطوة: del l[i:j:k]
  • الإزالة: l.remove(x) لحذف أول ورود للعنصر
  • النزع: l.pop([i]) أخذ العنصر من الموضع (مع حذفه من ذلك الموضع)
    • إن لم يحدد الموضع: نزع الأخير. إذ القوسان [i] هنا في التعريف يعبران عن عامل اختياري وهو الموضع i
  • الإدراج: l.insert(i, x) لإضافة عنصر في موضع محدد
  • الإلحاق: l.append(x) لإضافة عنصر في النهاية
  • الترتيب: l.sort() أو بالإجراء المبني sorted(l)
  • العكس: l.reverse() أو بالإجراء المبني reversed(l)

لاحظ رسالة الخطأ عند محاولة التعديل على الصف، الذي نعرفه بالقوسين المنحنيين ()، إذْ هو جامد لا يقبل التغير:

t = (10, 20, 30, 40, 50)
t[0] = 100
print(t)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[157], line 2
      1 t = (10, 20, 30, 40, 50)
----> 2 t[0] = 100
      3 print(t)

TypeError: 'tuple' object does not support item assignment

لكن هذا مقبول في القائمة، التي نعرفها بالقوسين المربعين []، لأنها متغيرة:

l = [10, 20, 30, 40, 50]
l[0] = 100
print(l)
[100, 20, 30, 40, 50]

الاستبدال بالموضع والحذف منه:

l = [10, 20, 30, 40, 50]
l[0] = 100
assert l == [100, 20, 30, 40, 50]

del l[0]
assert 100 not in l

الاستبدال بالقطعة والحذف منها

l = [10, 20, 30, 40, 50]
l[1:3] = [200, 300]
assert l == [10, 200, 300, 40, 50]

del l[1:3]
assert l == [10, 40, 50]

الإدراج:

l = [10, 20, 30, 40, 50]
l.insert(1, 100)
assert l == [10, 100, 20, 30, 40, 50]

الإزالة:

l = [10, 20, 30, 40, 50]
l.remove(20)
assert l == [10, 30, 40, 50]

الإلحاق:

l = [10, 20, 30, 40, 50]
l.append(60)
assert l == [10, 20, 30, 40, 50, 60]

الترتيب والعكس:

l = [30, 40, 10, 20, 50]
l.sort()
assert l == [10, 20, 30, 40, 50]

l.reverse()
assert l == [50, 40, 30, 20, 10]

نزع العنصر الأخير:

l = [10, 20, 30, 40, 50]
x = l.pop()
assert x == 50
assert 50 not in l

النطاق (range)

يمثل النطاق (range) مولِّدًا لسلسلة أعداد في نطاق محدد ببداية ونهاية، وبين كل عدد والذي يليه مسافة محددة. فثلاثة عوامل تحدده:

  1. البداية (start=0):
    • مشمولة
    • قيمتها الابتدائية 0 (إذا أهملت)
  2. النهاية (stop):
    • غير مشمولة
    • وهي واجبة (إهمالها ممتنع)
  3. الخطوة (step=1):
    • مقدار الزيادة أو النقص للعدد في كل كرة
    • قيمتها الابتدائية 1 (إذا أهملت)

دعونا الآن نلقي نظرة على التعريف كما هو موجود في وثائق بايثون، وذلك لنتعلم كيف نقرؤ التعريف. ادخل الرابط وتأمل معي ..

  • class range(stop)
  • class range(start, stop[, step])

أولا: تدل كلمة class على أنها معرَّفة كنوع، فيكون طلب الإجراء بنفس الاسم range للإنشاء.

ثانيًا: نلاحظ أن لدينا تعريفان؛ وهما مختلفان، فأيهما يكون؟

نجيب عن ذلك فنقول: التعريف الأوَّل يُعمل به إذا حددنا عاملاً واحدًا؛ فيكون العامل هو stop وتأخذ البداية والخطوة قيمتهما الابتدائية: start=0 و step=1 حسب ما كُتب:

If the step argument is omitted, it defaults to 1.

If the start argument is omitted, it defaults to 0

for i in range(5):
    print(i)
0
1
2
3
4

أما التعريف الثاني فيجب تفكيكه لنفهمه: class range(start, stop[, step]).

وجود الأقواس المربعة [ ] يعني أجزاءً اختياريَّة. فإذًا؛ الجزء الإلزامي هو start, stop؛ فإن عينَّا قيمتين، فتكون الأولى البداية، والثانية النهاية، وتبقى الخطوة على قيمتها الابتدائية step=1.

for i in range(5, 10):
    print(i)
5
6
7
8
9

أما إذا عينت الثلاثة جميعًا فسيكون الأول start والثاني stop والثالث step:

for i in range(0, 10, 2):
    print(i)
0
2
4
6
8

ولك أن تعكس النطاق بتعيين step بقيمة سالبة، لكن يجب حينها أن تجعل البداية أعلى من النهاية:

for i in range(10, 0, -1):
    print(i)
10
9
8
7
6
5
4
3
2
1

التكرار والإشارة

xs = [10, 20, 30, 40, 50, 60]

ويُسرَد التسلسل بكلمة for، على النحو التالي:

for x in xs:
    print(x)
10
20
30
40
50
60

أو بسرد النطاق (حيث النهاية تكون: len(xs) أي: طول التسلسل) واستعمال الإشارة بالموضع (xs[i])، نحو:

for i in range(len(xs)):
    print(xs[i])
10
20
30
40
50
60

فهذا يفيد في التحكم في السرد، فلو أردنا كل عنصرٍ ثانٍ، نجعل الخطوة 2 ابتداء من العنصر الثاني 1، فنكتبها هكذا:

for i in range(1, len(xs), 2):
    print(xs[i])
20
40
60

أو أردنا قراءة الموضع والذي قبله، فهكذا:

for i in range(1, len(xs), 2):
    print(xs[i-1], xs[i])
10 20
30 40
50 60

فإن جوَّزنا التداخل، جعلنا الخطوة 1، هكذا:

for i in range(1, len(xs), 1):
    print(xs[i-1], xs[i])
10 20
20 30
30 40
40 50
50 60

وهلم جرا..

تأجيل النتيجة

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

نستعمل فعل الإنشاء range() لإنشاء نطاق:

r = range(0, 20, 2)
r
range(0, 20, 2)

فحين نسألن عن عضوية عنصر ما في النطاق؛ يتم حساب النطاق بحسبه:

print(11 in r)
print(10 in r)
False
True

كذلك الإجراء عند البحث عن موضع رقمٍ ما:

print(r.index(10))
5

والإشارة لموضع ما أو قطعة كذلك:

print(r[5])
print(r[:5])
print(r[-1])
10
range(0, 10, 2)
18

تحقيق النطاق

المولِّد لا تتحقق عناصره إلا عند الحاجة إليها؛ أي: عند قراءتها. فإذا جعلناه عاملاً في جملة الإنشاء list؛ تولَّدَت جميع عناصره ووُضِعَت في قائمة:

evens = list(range(0, 10, 2))
odds = list(range(1, 10, 2))
print(evens)
print(odds)
[0, 2, 4, 6, 8]
[1, 3, 5, 7, 9]

ضم التسلسلات المرتبطة (zip)

هذان تسلسلان مرتبطان:

students = ['Ahmad', 'Belal', 'Camal', 'Dawud', 'Emad']
marks = [90, 80, 75, 85, 95]

assert len(students) == len(marks)

ويُمكن ضمُّ السلسلتين بحيث ينتج في التكرار عُنصران في كل مرة؛ وذلك بفعل الضم zip() الذي يُنتج مُكَرَّرًا -بفتح الراء- (Iterable). فإذا ضممنا سلسلتين، خرج لنا في كل كرَّة زوج (x, y):

for x, y in zip(students, marks):
    print(x, y)
Ahmad 90
Belal 80
Camal 75
Dawud 85
Emad 95

flowchart LR
    students --> zip
    marks --> zip
    zip --> for
    for --> x
    for --> y

ضم المكررات

ولاحظ فيما يلي أن:

  • نوع students قائمة (list)
  • ونوع marks نطاق (range)
  • ونوع classes صف (tuple)

ومع ذلك فإنه يجوز ضمُّها لأن الإجراء يقبلُ كُل ما هو مُكَرَّر:

students = ['Ahmad', 'Belal', 'Camal', 'Dawud', 'Emad']
marks = range(75, 95+1, 5)
classes = ('A-1', 'A-1', 'A-2', 'A-1', 'A-2')

assert list == type(students)
assert tuple == type(classes)
assert range == type(marks)

for x, y, z in zip(students, marks, classes):
    print(x, y, z)
Ahmad 75 A-1
Belal 80 A-1
Camal 85 A-2
Dawud 90 A-1
Emad 95 A-2

القراءة بالموضع المشترك

ويكون قراءة التسلسلات المرتبطة أيضًا بسرد مُكَرَّر النطاق، والإشارة إلى كل عنصر بالموضع:

for i in range(len(students)):
    x, y, z = students[i], marks[i], classes[i]
    print(x, y, z)
Ahmad 75 A-1
Belal 80 A-1
Camal 85 A-2
Dawud 90 A-1
Emad 95 A-2

الإنشاء المختصر: الجملة الثلاثية

مما تميزت به لغة بايثون عن غيرها: مختصرة الإنشاء (Comprehension)؛ وهي جملة تُنشئ مجموعة مستمَدَّة من مكرر في ثلاث جُمَل في سطرٍ واحدٍ -غالبًا- ووظيفتها: إنشاء مجموعة مستمَدَّة من مكرر.

وليسَت زيادتها في اللغة من باب الضرورة وإنما من باب التحسين. إذْ فيها قوة في التعبير عن جمل كثيرة في مساحة صغيرة. فهذا المثال يعبر عن إنشاء قائمة كل عنصرٍ فيها مربَّعٌ من المكرر range(10) في سطرٍ واحد:

squares = [x ** 2 for x in range(10)]
squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

فهي جملة إنشاء مركَّبة من ثلاث جمل:

  1. تعبير (x ** 2) ، الذي يشتمل غالبًا على متغير التكرار (x)
  2. تكرار: (for x in range(10))
  3. وشرط: والشرطُ ليسَ بشرط؛ لذا جاز إهماله في هذا المثال

فهي مكافئة للقطعة التالية:

squares = []
for x in range(10):
    squares.append(x ** 2)
squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

ولو أردنا ترشيح الأعداد الزوجية من قائمة، نستطيع استعمال جملة الشرط في الاختصار على النحو التالي:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
evens = [x for x in numbers if x % 2 == 0]
evens
[0, 2, 4, 6, 8]
  1. التعبير: (x) فقط
  2. التكرار: (for x in numbers)، وتذكر أن القائمة مُكرَّر
  3. الشرط: (if x % 2 == 0)

وهي مكافئة للقطعة التالية:

evens = []
for x in numbers:
    if x % 2 == 0:
        evens.append(x)
evens
[0, 2, 4, 6, 8]

وأما القوسان المربعان [ ] -في كلا المثالين- فلإنشاء قائمة. وبحسب ما يُراد إنشاؤه تختلف الأقواس:

  • [expression for item in iterable if condition] للقائمة (list)
  • (expression for item in iterable if condition) للمولِّد (Generator)
  • {expression for item in iterable if condition} لمجموعة الفرائد (set) وسيأتي الكلام عنها في الباب القادم
  • {expression: expression for item in iterable if condition} للقاموس (dict) وسيأتي الكلام عنه في الباب القادم