6  المجموعة المرتبة

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

وكل ما هو من جنس المجموعة (Collection) فإنه يقبل الأفعال التالية:

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

6.1 التسلسل

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

  • مجموعة: يعني قبوله الأفعال الثلاثة السابق ذكرها.
  • مرتبة: يعني أن لكل عنصر موضعًا فيها، وله ما قبله وما بعده.

وسوف نرمز للمفرد بـx ولما يدل على التسلسل بـs.

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

  1. القائمة (list) ويُعبَّرُ عنه بالقوسين المربعين [].
  2. الصف (tuple) ويُعبَّرُ عنه بالقوسين المنحنيين ().
  3. المجال (range) ويُعبَّرُ عنه بالفعل المنشئ range().
  4. النص (str) ويُعبَّرُ عنه بالتنصيص المفرد '' أو المزدوج "".

فهذه الأربعة تقبل الأفعال التالية:

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

وتقبل من أفعال الإنشاء:

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

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

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

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

ومن جهة كونها عوامل للفعل؛ فإن الجامد لا يقبل أن يكون مفعولاً.

الإنشاء

  • تنشأ القائمة بوضع العناصر بين القوسين المربعين [] أو باستعمال الفعل المنشئ list()، وهي تسلسل متغير.
  • ينشأ الصف بوضع العناصر بين القوسين المنحنيين () أو باستعمال الفعل المنشئ tuple()، وهو تسلسل جامد.
s = (10, 20, 30) + (40, 50)
s = s * 2
print(s)
(10, 20, 30, 40, 50, 10, 20, 30, 40, 50)

ويقبل إنشاء مجموعة من العناصر مختلفة النوع، بما في ذلك القائمة والصف كعنصر:

s = (10, 'A', 2.0, True, ['B', 30])
print(s)
(10, 'A', 2.0, True, ['B', 30])

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

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[start : end : step]. والقيم الابتدائية عند الإغفال هي: s[0:len(s):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               
 +--------------+--------------+--------------+
 | (10, 20, 30) | (40, 50, 60) | (70, 80, 90) |
 +--------------+--------------+--------------+
-3             -2             -1

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

 0    1    2            
 +----+----+----+
 | 10 | 20 | 30 |
 +----+----+----+
-3   -2   -1
 0    1    2            
 +----+----+----+
 | 40 | 50 | 60 |
 +----+----+----+
-3   -2   -1
 0    1    2            
 +----+----+----+
 | 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
 +---+---+---+---+---+
 | L | e | m | o | n |
 +---+---+---+---+---+
-5  -4  -3  -2  -1

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

البحث

s = ('A', 'B', 'A')
assert s.index('B') == 1
assert s.count('A') == 2
s = (30, 20, 40, 10, 50)
assert min(s) == 10
assert max(s) == 50

6.2 القائمة

القائمة (list) تسلسل متغير. وهذا يعني:

  • مُتَغَيِّرَة: يعني أنها تقبل الإضافة والحذف والتعديل على عناصرها
  • مُرَتَّبَة: يعني أن العناصر مرقَّمة بالتسلسل هكذا: [0, 1, 2, ...]
    • ويترتب عليه قبولها الإشارة بالموضع i أو بالقطعة [i:j] أو بالقطعة بالخطوة [i:j:k]

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

التغير

التغير هي الخاصية التي تختلف فيها القائمة عن قسيماتها التسلسلية. ومعناه قبولها الأفعال التالية (نستعمل في المثال حرف 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[10], 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

6.3 النطاق

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

  1. البداية (start=0):
    • مشمولة
    • (إن لم تعيَّن) وهي صفر بالابتداء
  2. النهاية (stop):
    • غير مشمولة
    • وهي واجبة (إهمالها ممتنع)
  3. الخطوة (step=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

أو بسرد نطاقٍ واستعمال الإشارة بالموضع، نحو:

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]

6.4 التسلسلات المرتبطة

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

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

assert len(students) == len(marks)

لو أردنا أن نمر على التسلسلين في نفس الوقت نستعمل الفعل المبني zip() الذي يظل يولد صفًا عناصره من كل تسلسل:

list(zip(students, marks))
[('Ahmad', 90), ('Belal', 80), ('Camal', 75), ('Dawud', 85), ('Emad', 95)]

وحاجتنا للفعل المنشئ list() بسبب أن zip مولِّدٌ مثل range لا يحسب العناصر إلا عند قراءتها. والإنشاء يقرأ جميعها لتظهر.

وعند سياقها في جملة التكرار فإنها تولد زوجًا في كل كرة، إذ هي متوالية (Iterable):

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

ولو كان لدينا ثلاثة تسلسلات فإنها تولد ثلاثة عناصر في كل كرة:

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

ولاحظ أن نوع students قائمة (list) ونوع marks نطاق (range)، ونوع classes صف (tuple)، لكن الفعل zip يقبل متسلسلات من أي نوع. بل هو في الحقيقة يقبل أي متوالية (Iterable)؛ والتسلسل متوالية (Sequence -> Iterable).

فتلك الطريقة البايثونية. انظر التوثيق للمزيد عن zip().

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

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

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

مما تميزت به لغة بايثون عن غيرها: مختصرة الإنشاء (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) وسيأتي الكلام عنه في الباب القادم

وقد يكون التعبير غير مشتمل على متغير التكرار، نحو:

numbers = range(11)
evens_count = (1 for x in numbers if x % 2 == 0)

فقد أنشأنا بالقوسين الدائريين ( ) مولِّدًا يُنتج 1 لكل عنصر زوجي، بعدد العناصر الزوجية في المتوالية numbers. ثم نحقق وجوده بالقراءة، فنقول مثلاً: sum لجمعها كلها:

sum(evens_count)
6

أو نضعها في جملة واحدة:

sum(1 for x in numbers if x % 2 == 0)
6