تعلم البرمجة مع Python


الدرس: اكتشف حلقة for


الصفحة السابقة
فيما يلي بعض الفصول ، درسنا الحلقات. لا تنزعج ، ما رأيناه لا يزال ذا صلة حتى اليوم ... لكننا سنحفر أعمق في الموضوع ، الآن بعد أن نستكشف عالم الكائن.

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

التكرارات



لقد استخدمنا التكرارات دون علم عندما تطرقنا لأول مرة الى الحلقات ، والأهم من ذلك ، منذ استخدمنا الكلمة الأساسية for للتكرار خلال كائنات الحاوية.


ma_liste = [1, 2, 3]
for element in ma_liste:
 

استخدم التكرارات


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

عندما تصادف Python سطرًا من النوع for element in ma_liste: ، فإنها ستستدعي مكرر ma_liste . المكرر هو كائن سيكون مسؤولاً عن التكرار خلال كائن الحاوية ، وهنا قائمة.

يتم إنشاء المكرر بطريقة خاصة __iter__ للكائن. هنا ، فإن طريقة  __iter__ الفئة list هي التي يتم استدعاؤها والتي تعيد مكررًا يسمح بتصفح القائمة.

في كل حلقة ، تستدعي Python  __next__ طريقة التكرار الخاصة ، والتي يجب أن تعيد العنصر التالي في المسار أو تعيد الاستثناء StopIteration إذا اقتربت الجولة من نهايتها.

قد لا يكون الأمر واضحًا جدًا ... لذلك دعونا نرى مثالاً.

قبل الغوص في التعليمات البرمجية، عليك ان تعرف ان بايثون يستخدم وظيفتين للاتصال والتلاعب مع المكررات:  iter يسمح باستدعاء الأسلوب الخاص  __iter__ من الكائن الذي تم تمريره كمعلمة و next يستدعي الأسلوب الخاص  __next__للمكرر الذي وقع تمريره كمعلمة.


>>> ma_chaine = "test"
>>> iterateur_de_ma_chaine = iter(ma_chaine)
>>> iterateur_de_ma_chaine

>>> next(iterateur_de_ma_chaine)
't'
>>> next(iterateur_de_ma_chaine)
'e'
>>> next(iterateur_de_ma_chaine)
's'
>>> next(iterateur_de_ma_chaine)
't'
>>> next(iterateur_de_ma_chaine)
Traceback (most recent call last):
  File "", line 1, in 
StopIteration
>>>
 
  • نبدأ بإنشاء سلسلة من الأحرف (حتى الآن ، لا شيء معقد).
  • ثم يتم استدعاء الوظيفة iter عن طريق تمرير السلسلة كمعامل. تستدعي هذه الوظيفة الطريقة الخاصة __iter__ للسلسلة ، والتي ترجع المكرر للتكرار ma_chaine .
  • سنقوم بعد ذلك باستدعاء الوظيفة عدة مرات ، next لتمرير المكرر كمعامل. تستدعي هذه الوظيفة __next__ طريقة التكرار الخاصة . تقوم بإرجاع كل حرف متضمن في السلسلة الخاصة بنا على التوالي وتلقي استثناءً StopIteration عندما يتم مسح السلسلة بالكامل.
عندما نمر عبر سلسلة بحلقة  for ( for lettre in chaine:) ، فإن آلية التكرار هذه هي التي تسمى. كل حرف يتم إرجاعه بواسطة مكررنا موجود في المتغير lettre وتتوقف الحلقة عند رفع الاستثناء StopIteration .

يمكنك استخدام هذا الكود مع كائنات حاوية أخرى ، قوائم على سبيل المثال.

دعونا ننشئ مكرراتنا


على سبيل المثال ، سننشئ فئتين:

  • RevStr : فئة موروثة str منها ستكون مضمونة لإعادة تعريف الطريقة __iter__ . وبالتالي سيتم تغيير طريقة انتقالها: فبدلاً من اجتياز السلسلة من اليسار إلى اليمين ، سيتم اجتيازها من اليمين إلى اليسار (من الحرف الأخير إلى الأول).
  • ItRevStr : مكررنا. سيتم إنشاؤه من طريقة __iter__ لـ RevStr وسوف تضطر إلى الذهاب من خلال سلسلة لدينا من الحرف الأخير إلى الأول.
هذه الآلية جديدة قليلاً ، أضع الكود لك دون الكثير من التشويق. إذا كنت ترغب في القيام بالتمرين ، فلا تتردد ، لكنني سأمنحك الفرصة للتدرب من الفصل التالي على أي حال.


class RevStr(str):
    """ فئة تحتوي على طرق وخصائص السلاسل المركبة
        نحن فقط نحدد طريقة المرور    str   من
    مختلف: بدلاً من المرور بالسلسلة من الأول إلى الأخير
    حرف ، نمر عليه من الأخير إلى الأول.
    
    لا تحتاج الطرق الأخرى ، بما في ذلك المنشئ
    لإعادة تعريفها """
    
    def __iter__(self):
        """ تقوم هذه الطريقة بإرجاع مكرر يتكرر خلال السلسلة
        'str'في الاتجاه المعاكس """
        
        return ItRevStr(self) # نعيد المكرر الذي تم إنشاؤه لهذه المناسبة

class ItRevStr:
    """ مكرر يسمح بتصفح سلسلة من الحرف الأخير
     الى الاول. نقوم بتخزين الموقف الحالي و
     سلسلة للتصفح """
    
    def __init__(self, chaine_a_parcourir):
       """ نضع أنفسنا في نهاية السلسلة """
        self.chaine_a_parcourir = chaine_a_parcourir
        self.position = len(chaine_a_parcourir)
    def __next__(self):
        """ يجب أن تعيد هذه الطريقة العنصر التالي في المسار ،
         أو رفع استثناء "StopIteration" إذا تم إنهاء المسار """
        
        if self.position == 0: # نهاية الدورة
            raise StopIteration
        self.position -= 1 
        return self.chaine_a_parcourir[self.position]
 
الآن يمكنك إنشاء سلاسل يجب أن تنتقل من الحرف الأخير إلى الأول.

>>> ma_chaine = RevStr("Bonjour")
>>> ma_chaine
'Bonjour'
>>> for lettre in ma_chaine:
...     print(lettre)
... 
r
u
o
j
n
o
B
>>>
 
لاحظ أنه من الممكن أيضًا تنفيذ الطريقة مباشرةً __next__ في كائن الحاوية الخاص بنا. في هذه الحالة ، ستكون الطريقة __iter__ قادرة على ان تُعيد self . يمكنك أن ترى مثالاً ، استوحى منه الكود أعلاه ، في توثيق بايثون .

لا يزال ثقيلًا نوعًا ما ، أليس كذلك ، هل يتعين عليك القيام بالمكررات في كل مرة؟ خاصة إذا كان لابد من تصفح كائنات الحاوية الخاصة بنا بعدة طرق ، مثل القواميس على سبيل المثال.

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

حان الوقت الآن لإلقاء نظرة على المولدات.

المولدات



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

مولدات بسيطة


لإنشاء مولدات، سنكتشف كلمة رئيسية جديدة:   yield . لا يمكن استخدام هذه الكلمة الأساسية إلا في جسم الوظيفة وتتبعها قيمة لإرجاعها.

انتظر لحظة ... قيمة؟ لإرسالها مرة أخرى؟

نعم. مبدأ المولدات كونه خاصًا قليلاً ، فإنه يتطلب كلمة رئيسية لنفسه. الفكرة هي تحديد وظيفة لنوع المسار. عندما تطلب العنصر الأول من المسار (بفضل next ) ، تبدأ الوظيفة في تنفيذها. بمجرد أن تواجه تعليمة yield، فإنها تُرجع القيمة التالية وتتوقف مؤقتًا. عندما تسأل عن العنصر التالي من الكائن (شكرًا ، مرة أخرى ، لـ next ) ، يستأنف التنفيذ من حيث توقف ويتوقف عند yield التالي ... وهكذا. في نهاية تنفيذ الوظيفة ، StopIteration يتم طرح الاستثناء تلقائيًا بواسطة Python .

سنأخذ مثالًا بسيطًا جدًا للبدء:


>>> def mon_generateur():
...     """ أول مولد لدينا. سيعيد ببساطة 1 و 2 و 3"""
...     yield 1
...     yield 2
...     yield 3
...
>>> mon_generateur

>>> mon_generateur()

>>> mon_iterateur = iter(mon_generateur())
>>> next(mon_iterateur)
1
>>> next(mon_iterateur)
2
>>> next(mon_iterateur)
3
>>> next(mon_iterateur)
Traceback (most recent call last):
  File "", line 1, in 
StopIteration
>>>
 
أعتقد أن هذا يذكرك بشيء! هذه الوظيفة ، بصرف النظر عن استخدام yield، كلاسيكية إلى حد ما. عند تشغيلها ، ينتهي بك الأمر مع مولد. هذا المولد هو كائن تم إنشاؤه بواسطة Python والذي يحدد أسلوبه الخاص __iter__وبالتالي مكرره الخاص. ربما قمنا بما يلي:


for nombre in mon_generateur(): # انتباه ننفذ الوظيفة
    print(nombre)
 
هذا لا يزال يجعل الكود أسهل في الفهم.

لاحظ أنه يجب علينا تنفيذ الوظيفة mon_generateur للحصول على مولد. إذا حاولت التكرار من خلال وظيفتنا ( for nombre in mon_generateur) فلن تعمل.

بالطبع ، في معظم الأوقات ، لن نسميها كذلك yield . منشئ مثالنا لا يهتم كثيرًا ، يجب التعرف عليه.

دعنا نحاول القيام بشيء أكثر فائدة: مولد يأخذ كمعلمات عددين صحيحين ، حد أدنى وحد أعلى ، ويعيد كل عدد صحيح بين هذه الحدود. إذا كتبنا على سبيل المثال intervalle(5, 10)، فسنكون قادرين على تصفح الأعداد الصحيحة من 6 إلى 9.

وبالتالي فإن النتيجة المتوقعة هي:


>>> for nombre in intervalle(5, 10):
...     print(nombre)
... 
6
7
8
9
>>>
 
يمكنك محاولة القيام بالتمرين ، فهو تمرين جيد وغير معقد للغاية.

هذا هو الإصلاح:


def intervalle(borne_inf, borne_sup):
    """  borne_inf و  borne_sup     المولد يمر بسلسلة الأعداد الصحيحة بين 
    
    ملاحظة: يجب أن يكون الحد الأدنى أقل من الحد الأعلى """
    
    borne_inf += 1
    while borne_inf < borne_sup:
        yield borne_inf
        borne_inf += 1
 
مرة أخرى ، يمكنك تحسين هذه الوظيفة. لماذا لا تتأكد من أنه إذا كان الحد الأدنى أكبر من الحد الأعلى ، فإن المسار يتم في الاتجاه الآخر؟

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

إذا احتوت الطريقة الخاصة ، في أي فئة ، __iter__على استدعاء لـ  yield، فسيتم استدعاء هذا المولد عندما نريد المرور عبر الحلقة. حتى عندما تمر Python عبر المولدات ، كما رأيت ، فإنها (ضمنيًا) تستخدم المكرر. إنها مريحة أكثر للمبرمج ، لا نحتاج إلى إنشاء فئة مكرر أو رمز طريقة  __next__ ، أو حتى طرح الاستثناء  StopIteration : Python تفعل كل هذا لنا

المولدات كإجراءات مشتركة co-routines


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

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

co-routines هي وسيلة لايقاف التكرار... للحلقة. على سبيل المثال ، في المولد الخاص بنا intervalle، قد نرغب في الانتقال مباشرة من 5 إلى 10.

يتم تضمين نظام الإجراءات المشتركة في Python في الكلمة الأساسية yield التي رأيناها أعلاه واستخدام طرق معينة للمولد الخاص بنا.

كسر الحلقة

الطريقة الأولى التي سنراها هي close . يسمح بمقاطعة الحلقة قبل الأوان ، مثل الكلمة الرئيسية break باختصار.


generateur = intervalle(5, 20)
for nombre in generateur:
    if nombre > 17:
        generateur.close() # إيقاف الحلقة
 
كما ترى ، لاستدعاء طرق المولد ، علينا تخزينها في متغير قبل الحلقة. إذا كنت قد كتبت مباشرة for nombre in intervalle(5, 20)، فلن تتمكن من استدعاء طريقة closeالمولد.

إرسال البيانات إلى مولدنا


في هذا المثال ، سنقوم بتوسيع المولد الخاص بنا لقبول تلقي البيانات أثناء تشغيله.

يتم إجراء نقطة تبادل البيانات باستخدام الكلمة الأساسية yield .  yield valeur "تُرجع "  valeurالتي ستصبح بالتالي القيمة الحالية للمسار. ثم يتم إيقاف الوظيفة مؤقتًا. يمكننا ، في هذه اللحظة ، إرسال قيمة إلى مولدنا. هذا يسمح بتعديل عمل المولد الخاص بنا أثناء الرحلة.

لنأخذ مثالنا من خلال دمج هذه الوظيفة:


def intervalle(borne_inf, borne_sup):
    """ _inf و borne_sup  المولد يمر بسلسلة الأعداد الصحيحة بين.
     يجب أن يكون مولدنا قادرًا على "القفز" على نطاق معين من الأرقام
     حسب القيمة المعطاة لها خلال الرحلة. ال
     borne_inf القيمة التي يتم تمريرها إليها هي القيمة الجديدة لـ.    
    ملاحظة: يجب أن يكون الحد الأدنى أقل من الحد الأعلى """
    borne_inf += 1
    while borne_inf < borne_sup:
        valeur_recue = (yield borne_inf)
        if valeur_recue is not None: # تلقى مولدنا شيئًا
            borne_inf = valeur_recue
        borne_inf += 1
 
نقوم بتهيئة المولد الخاص بنا لقبول قيمة محتملة أثناء التكرار. إذا تلقت قيمة ، فسوف تقوم بتعيينها إلى نقطة التكرار.

بمعنى آخر ، أثناء الحلقة يمكنك أن تطلب من المولد القفز إلى 20 على الفور إذا كان الرقم 15.

كل شيء يحدث من خط yield . بدلاً من مجرد إرجاع قيمة إلى الحلقة ، فإننا نلتقط أي قيمة في valeur_recue . بناء الجملة بسيط variable = (yield valeur_a_renvoyer) . لا تنس الأقواس الموجودة حول yield valeur .

إذا لم يتم تمرير أي قيمة إلى المولد الخاص بنا ، valeur_recue فستكون القيمة لدينا None . لذلك نتحقق مما إذا كان الأمر لا يساوي None، وفي هذه الحالة ، نقوم بتعيين القيمة الجديدة لـ borne_inf .

هذا هو الكود للتفاعل مع مولدنا. نستخدم طريقة send إرسال قيمة إلى المولد الخاص بنا:


generateur = intervalle(10, 25)
for nombre in generateur:
    if nombre == 15: # نقفز إلى 20
        generateur.send(20)
    print(nombre, end=" ")
 
هناك طرق أخرى للتفاعل مع مولدنا. يمكنك العثور عليها ، بالإضافة إلى توضيحات إضافية ، في الوثائق الرسمية التي تتناول الكلمة الرئيسيةyield  .

باختصار


  • عندما نستخدم الحلقة for element in sequence:، يسمح مكرر هذا التسلسل بالمرور خلالها.
  • يمكننا استرداد مكرر التسلسل باستخدام الوظيفة iter .
  • تسلسل يعيد المكرر ويسمح له بالمرور باستخدام الطريقة الخاصة __iter__ .
  • المكرر له طريقة خاصة ، __next__والتي تعيد العنصر التالي للتكرار أو يرمي الاستثناء StopIteration الذي يوقف الحلقة.
  • تجعل المولدات من السهل إنشاء التكرارات.
  • هذه وظائف تستخدم الكلمة الأساسية  yield متبوعة بالقيمة لتمريرها إلى الحلقة.