المولّدات
أهداف التعلّم
بعد هذا القسم
- ستعرف ما هو مولّد بايثون (generator)
- ستكون على دراية بالكلمة المفتاحية
yield- ستكون قادرًا على كتابة دوال المولّد الخاصة بك
لقد صادفنا بالفعل حالات نتعامل فيها مع سلسلة من العناصر، ونحتاج فيها إلى العنصر التالي أو العناصر التالية من السلسلة، لكننا لا نريد بالضرورة أن نبني السلسلة كاملة حتى تلك النقطة في كل مرة يُطلب فيها عنصر جديد. وبعض المتتاليات التعاودية، مثل متتالية فيبوناتشي، مثال جيد على هذه الحالة. فإذا كان كل نداء دالة يولّد السلسلة كاملة تعاوديًا حتى النقطة المطلوبة، فإننا ننتهي إلى توليد بداية السلسلة مرات عديدة.
إن المولّدات في بايثون طريقة لإنتاج العنصر التالي فقط من السلسلة عند الحاجة إليه، فهي تشغّل عملية توليد السلسلة مرة واحدة فقط (بالنسبة لتنفيذ معيّن للبرنامج). وهي تعمل في معظم الأحيان مثل الدوال العادية، إذ يمكن نداؤها وستعيد قيمًا، لكن القيمة التي تعيدها دالة المولّد تختلف عن الدالة العادية. ينبغي للدالة العادية أن تعيد القيمة نفسها في كل مرة إذا أُعطيت الوسائط نفسها. أما دالة المولّد فينبغي لها أن تتذكّر حالتها الحالية وأن تعيد العنصر التالي من السلسلة، والذي قد يختلف عن العنصر السابق.
وكما توجد طرق عديدة لحل أي مسألة برمجية تقريبًا، توجد طرق عديدة لتحقيق وظيفة مشابهة للمولّدات، لكن المولّدات يمكن أن تساعد في جعل البرنامج أسهل فهمًا، وأن توفّر في حالات معيّنة الذاكرة أو موارد حوسبية أخرى.
الكلمة المفتاحية yield
ينبغي أن تحتوي دالة المولّد على الكلمة المفتاحية yield، وهي التي تحدّد القيمة التي تعيدها الدالة. لنلقِ نظرة على دالة تولّد أعدادًا صحيحة، بدءًا من الصفر وانتهاءً بقيمة قصوى محدّدة سلفًا:
def counter(max_value: int):
number = 0
while number <= max_value:
yield number
number += 1
يمكن الآن تمرير الدالة counter وسيطًا إلى الدالة next():
if __name__ == "__main__":
numbers = counter(10)
print("First value:")
print(next(numbers))
print("Second value:")
print(next(numbers))
First value:
0
Second value:
1
كما ترى من المثال أعلاه، تشبه الكلمة المفتاحية yield الكلمة المفتاحية return: فكلتاهما تُستخدم لتحديد قيمة الإرجاع. والفرق أن yield لا «تُغلق» الدالة بالمعنى نفسه الذي يفعله return. فدالة المولّد التي تحتوي على الكلمة المفتاحية yield تتابع حالتها، وفي المرة التالية التي تُنادى فيها ستواصل من الحالة نفسها.
يحتاج هذا المولّد أيضًا إلى قيمة قصوى، وقد كانت 10 في المثال أعلاه. وعندما تنفد القيم في المولّد، فإنه يرفع استثناء StopIteration:
if __name__ == "__main__":
# creates a generator with maximum value 1
numbers = counter(1)
print(next(numbers))
print(next(numbers))
print(next(numbers))
0
1
Traceback (most recent call last):
File "generator_example.py", line 11, in <module>
print(next(numbers))
StopIteration
يمكن التقاط الاستثناء باستخدام كتلة try - except:
if __name__ == "__main__":
numbers = counter(1)
try:
print(next(numbers))
print(next(numbers))
print(next(numbers))
except StopIteration:
print("ran out of numbers")
0
1
ran out of numbers
يمكن المرور على جميع العناصر في مولّد بسهولة باستخدام حلقة for:
if __name__ == "__main__":
numbers = counter(5)
for number in numbers:
print(number)
0
1
2
3
4
5
لا يلزم أن يكون للمولّدات قيمة قصوى محدّدة أو نقطة انتهاء. فيمكنها توليد القيم بلا نهاية (ضمن القيود الحوسبية والفيزيائية الأخرى، بطبيعة الحال).
لكن انتبه: لا ينجح المرور على مولّد بحلقة for إلا إذا انتهى المولّد في مرحلة ما. فإذا بُني المولّد على حلقة لا نهائية، فإن محاولة المرور عليه بحلقة for بسيطة ستؤدي إلى تنفيذ لا نهائي، تمامًا كما تفعل حلقة while بلا شرط انتهاء أو توقف.
تمرين برمجي: الأعداد الزوجية
اكتب دالة مولّد باسم
even_numbers(beginning: int, maximum: int)تأخذ عددين صحيحين وسيطين لها. وينبغي أن تنتج الدالة الأعداد الزوجية بدءًا منbeginningوانتهاءً بـmaximumعلى الأكثر.
مثالان على كيفية عمل الدالة:
numbers = even_numbers(2, 10)
for number in numbers:
print(number)
2
4
6
8
10
numbers = even_numbers(11, 21)
for number in numbers:
print(number)
12
14
16
18
20
تمرين برمجي: الأعداد الأولية
العدد الأولي هو عدد لا يقبل القسمة إلا على نفسه وعلى العدد 1. ووفقًا للاصطلاح تُعرَّف الأعداد الأولية بأنها الأعداد الصحيحة الموجبة بدءًا من العدد 2. والأعداد الأولية الستة الأولى هي 2 و3 و5 و7 و11 و13.
اكتب دالة مولّد prime_numbers() تُنشئ مولّدًا جديدًا. وينبغي أن يعيد المولّد أعدادًا أولية جديدة، واحدًا تلو الآخر بالتتابع، بدءًا من 2. ملاحظة: هذا المولّد لا ينتهي أبدًا. وسيولّد الأعداد ما دام هناك حاجة إليها.
على سبيل المثال:
numbers = prime_numbers()
for i in range(8):
print(next(numbers))
2
3
5
7
11
13
17
19
تلميح: يمكنك استخدام حلقة للتحقق مما إذا كان العدد أوليًا. فإذا كنا نتحقق من العدد x، فستمر الحلقة على الأعداد من 2 إلى x-1. وإذا كان x يقبل القسمة على أيٍّ منها، فهو ليس عددًا أوليًا.
إدراكات المولّد
لا تحتاج بالضرورة إلى تعريف دالة لإنشاء مولّد. فيمكننا بدلًا من ذلك استخدام بنية مشابهة لإدراك القائمة. وهذه المرة نستخدم الأقواس الدائرية للدلالة على مولّد بدلًا من قائمة أو قاموس:
# This generator returns squares of integers
squares = (x ** 2 for x in range(1, 64))
print(squares) # the printout of a generator object isn't too informative
for i in range(5):
print(next(squares))
<generator object <genexpr> at 0x000002B4224EBFC0>
1
4
9
16
25
في المثال التالي نطبع سلاسل فرعية من الأبجدية الإنجليزية، كل منها بطول ثلاثة أحرف. وهذا يطبع أول 10 عناصر في المولّد:
substrings = ("abcdefghijklmnopqrstuvwxyz"[i : i + 3] for i in range(24))
# print out first 10 substrings
for i in range(10):
print(next(substrings))
abc
bcd
cde
def
efg
fgh
ghi
hij
ijk
jkl
تمرين برمجي: كلمات عشوائية
اكتب دالة باسم
word_generator(characters: str, length: int, amount: int)تعيد مولّدًا جديدًا لتوليد كلمات عشوائية بناءً على المعاملات المعطاة.
تُولَّد الكلمة العشوائية باختيار عددٍ من الأحرف من السلسلة النصية المسماة characters بقدر ما يحدّده الوسيط length. ويمكن أن يظهر الحرف نفسه مرات عديدة في الكلمة العشوائية.
ويعيد المولّد عددًا من الكلمات بقدر ما يحدّده الوسيط amount قبل أن ينتهي.
مثال على تشغيل مولّد الكلمات:
wordgen = word_generator("abcdefg", 3, 5)
for word in wordgen:
print(word)
dbf
baf
ead
fga
ccc
ملاحظة: يعود إليك أمر تحديد كيفية تنفيذ هذه الدالة. فيمكنك استخدام مولّد «تقليدي» أو إدراك مولّد على حدٍّ سواء.