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


الدرس: تطبيق طرق خاصة


الصفحة السابقة
الطرق الخاصة هي طرق مثيل(instance) تتعرف عليها Python وتعرف كيفية استخدامها في سياقات معينة. يمكن استخدامها لإخبار Python بما يجب فعله عندما يأتي عبر تعبير مثل mon_objet1 + mon_objet2أو حتى mon_objet[indice] . وحتى أقوى من ذلك ، فهي تتحكم في كيفية إنشاء الكائن ، وكذلك الوصول إلى سماته.

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

تعديل الكائن والوصول إلى السمات



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

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

تعديل الكائن



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


class Exemple:
    """ مثال صغير على لفئة """
    def __init__(self, nom):
        """ مثال منشئ """
        self.nom = nom
        self.autre_attribut = " قيمة "
 
لإنشاء الكائن الخاص بنا ، نستخدم اسم الفئة ونمرر ، بين قوسين ، المعلومات التي يتوقعها المنشئ:


mon_objet = Exemple("un premier exemple")
 
لقد قمت بتبسيط ما يجري قليلاً ، ولكن هذا كل ما تحتاج إلى تذكره الآن. كما ترى ، منذ اللحظة التي يتم فيها إنشاء الكائن ، يمكننا الوصول إلى سماته باستخدام my_objet.name_attribute طرقه وتنفيذها باستخدام my_object.name_method (…) .

هناك أيضًا طريقة أخرى ، __del__والتي سيتم استدعاؤها عند تدمير الكائن.

تدمير ؟ متى يدمر الكائن نفسه؟


سؤال جيد. هناك عدة حالات: أولاً ، عندما تريد حذفه صراحة ، باستخدام الكلمة الأساسية del( del mon_objet) . ثم ، إذا تم إتلاف مساحة الاسم التي تحتوي على الكائن ، فسيتم تدمير الكائن أيضًا. على سبيل المثال ، إذا قمت بإنشاء مثيل للكائن في جسم الوظيفة: في نهاية استدعاء الوظيفة ، __del__سيتم استدعاء طريقة الكائن. أخيرًا ، إذا كان الكائن الخاص بك يقاوم كل الاحتمالات أثناء تنفيذ البرنامج ، فسيتم حذفه في نهاية التنفيذ.


def __del__(self):
        """ يتم استدعاء الأسلوب عند حذف الكائن """
        print("هذه هي نهاية ! أنا محذوف!")
 
ما فائدة السيطرة على تدمير كائن ما؟

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

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

تمثيل الكائن


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


<__main__.XXX object at 0x00B46A70>
 
بالتأكيد لدينا المعلومات المفيدة ، لكن ليس بالضرورة ما نريده ، والكل ليس رائعًا ، يجب الاعتراف به.

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


class Personne:
    """ فئة تمثل شخصًا """
    def __init__(self, nom, prenom):
        """ منشئ الفئة """
        self.nom = nom
        self.prenom = prenom
        self.age = 33
    def __repr__(self):
        """ عندما ندخل اسم الكائن في المترجم """
        return "Personne: nom({}), prénom({}), âge({})".format(
                self.nom, self.prenom, self.age)
 
والنتيجة بالصور:

>>> p1 = Personne("Micado", "Jean")
>>> p1
Personne: nom(Micado), prénom(Jean), âge(33)
>>>
 
كما ترى ، __repr__لا تأخذ الطريقة أي معاملات (باستثناء ، بالطبع ، self )وتعيد سلسلة نصية: السلسلة المراد عرضها عند إدخال الكائن مباشرة في المترجم.

يمكننا أيضًا الحصول على هذه السلسلة باستخدام الوظيفة repr، والتي تستدعي ببساطة الطريقة الخاصة __repr__للكائن الذي تم تمريره كمعامل:


>>> p1 = Personne("Micado", "Jean")
>>> repr(p1)
'Personne: nom(Micado), prénom(Jean), âge(33)'
>>>
 
هناك طريقة خاصة ثانية ، __str__تستخدم خصيصًا لعرض الكائن باستخدام  print . بشكل افتراضي ، إذا لم __str__يتم تحديد أي طريقة ، فإن Python تستدعي طريقة __repr__الكائن .  __str__يتم استدعاءها  أيضًا إذا كنت تريد تحويل الكائن الخاص بك إلى سلسلة باستخدام المنشئ  str .


class Personne:
    """ فئة تمثل شخصًا """
    def __init__(self, nom, prenom):
        """ منشئ الفئة """
        self.nom = nom
        self.prenom = prenom
        self.age = 33
    def __str__(self):
        """ طريقة لعرض كائننا بشكل أفضل """
        return "{} {}, âgé de {} ans".format(
                self.prenom, self.nom, self.age)
 
وفي الممارسة:

>>> p1 = Personne("Micado", "Jean")
>>> print(p1)
Jean Micado, âgé de 33 ans
>>> chaine = str(p1)
>>> chaine
'Jean Micado, âgé de 33 ans'
>>>
 

الوصول إلى سمات كائننا


سنكتشف ثلاث طرق لتحديد كيفية الوصول إلى سماتنا وتعديلها.

الطريقة__getattr__


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

مثال صغير؟


>>> class Protege:
...     """ فئة لها طريقة معينة للوصول إلى سماتها:
...     Noneإذا لم يتم العثور على السمة ، فإننا نعرض تنبيهًا ونعيد """
...
...     
...     def __init__(self):
...         """ نقوم بإنشاء بعض السمات بشكل افتراضي """
...         self.a = 1
...         self.b = 2
...         self.c = 3
...     def __getattr__(self, nom):
...         """ فإنها تستدعي name  من العثور على السمة المسماة Python إذا لم تتمكن ، 
...         هذه الطريقة. نعرض تنبيه """
...
...         
...         print("إنذار! لا توجد خاصية {} هنا!".format(nom))
...
>>> pro = Protege()
>>> pro.a
1
>>> pro.c
3
>>> pro.e
إنذار! لا توجد خاصية {} هنا!
>>>
 
هل تفهم المبدأ؟ إذا كانت السمة التي نريد الوصول إليها موجودة ، فلن يتم استدعاء طريقتنا. ومع ذلك ، إذا كانت السمة غير موجودة ، فسيتم استدعاء طريقتنا. __getattr__  نقوم بتمريرها كمعامل اسم السمة التي تحاول Python الوصول إليها. هنا ، نعرض تنبيهًا فقط. ولكن يمكننا إعادة التوجيه بسهولة إلى سمة أخرى. على سبيل المثال ، إذا حاولنا الوصول إلى سمة غير موجودة ، فإننا نعيد التوجيه إلى self.c . سأدعك تجربها ، إنها ليست صعبة.

الطريقة__setattr__


تحدد هذه الطريقة الوصول إلى السمة المراد تعديلها. إذا كنت أكتب objet.nom_attribut = nouvelle_valeur، وطريقة خاصة __setattr__وسوف يطلق على النحو التالي: objet.__setattr__("nom_attribut", nouvelle_valeur) . هنا مرة أخرى ، يتم تمرير اسم السمة المطلوبة في شكل سلسلة أحرف. تُستخدم هذه الطريقة لبدء إجراء بمجرد تعديل سمة ، على سبيل المثال حفظ الكائن:


def __setattr__(self, nom_attr, val_attr):
        """ object.nom_attr = val_attr تُستدعى الطريقة عندما نكتب  .
        نحن نحرص على حفظ الكائن """
        
        
        object.__setattr__(self, nom_attr, val_attr)
        self.enregistrer()
 
هناك تفسير جيد فيما يتعلق بالسطر 6 ، على ما أعتقد. سأبذل قصارى جهدي ، مع العلم أنني سأشرح مفهوم الميراث بمزيد من التفصيل في الفصل التالي. في الوقت الحالي ، ما عليك سوى معرفة أن جميع الفئات التي ننشئها موروثة من الفصل object . هذا يعني في الأساس أنهم يستخدمون نفس الأساليب.  يتم تعريف الفئة Object بواسطة Python . لقد قلت سابقًا ، إذا لم تحدد طريقة خاصة ، فإن Python لديها سلوك افتراضي: يتم تحديد هذا السلوك بواسطة الفصل object .

تم الإعلان عن معظم الطرق الخاصة بتنسيق object . إذا قمت بذلك على سبيل المثال objet.attribut = valeur دون تحديد طريقة __setattr__في فئتك ، فستكون طريقة __setattr__الفصل object التي سيتم استدعاؤها.

ولكن إذا تجاوزت الطريقة __setattr__في فئتك ، فستكون الطريقة التي تم استدعاؤها هي الطريقة التي تحددها ، وليس الطريقة الموجودة فيها object . نعم ولكن ... أنت لا تعرف كيف تعدل Python بالفعل قيمة السمة. الآلية وراء هذه الطريقة غير معروفة لك.

إذا حاولت ، في هذه الطريقة __setattr__، القيام بذلك self.attribut = valeur، فسوف تقوم بإنشاء خطأ لطيف: سوف ترغب Python في تعديل سمة ، وتستدعي طريقة __setattr__الفئة التي حددتها ، وتقع في هذه الطريقة على تعيين سمة جديدة ، يدعو مرة أخرى __setattr__ ... وكل هذا ، حتى ما يقرب من اللانهاية. تُنشئ Python حماية لمنع طريقة ما من استدعاء نفسها بلا حدود ، لكن هذا لا يحل المشكلة.


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

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

الطريقة__delattr__


يتم استدعاء هذه الطريقة الخاصة عندما نريد إزالة سمة من الكائن ، del objet.attribut على سبيل المثال عن طريق العمل . تأخذ كمعامل ، بالإضافة self إلى اسم السمة التي تريد حذفها. فيما يلي مثال لفصل لا يمكن إزالة أي سمات منه:


def __delattr__(self, nom_attr):
        """ لا يمكننا حذف سمة ، نحن نطرح الاستثناء
        AttributeError"""
        
        raise AttributeError("لا يمكنك إزالة أي سمة من هذه الفئة ")
 
مرة أخرى ، إذا كنت تريد إزالة سمة ، فلا تستخدمها في طريقتك del self.attribut . وإلا فإنك تخاطر بإثارة غضب بايثون! اذهب من خلال object .__delattr__يعرف كيف يعمل كل شيء بشكل أفضل مما نفعل.

مكافأة صغيرة


فيما يلي بعض الوظائف التي تؤدي إلى حد كبير ما فعلناه ولكنها تستخدم سلاسل لأسماء السمات. يمكنك استخدامها:


objet = MaClasse() # نقوم بإنشاء مثيل لفئتنا
getattr(objet, "nom") # object.name على غرار 
setattr(objet, "nom", val) # = objet.nom = val او objet.__setattr__("nom", val)
delattr(objet, "nom") # = del objet.nom او objet.__delattr__("nom")
hasattr(objet, "nom") # موجودة ، خطأ في خلاف ذلك  إذا كانت سمة True إرجاع " 
 
ربما لا ترى الكثير من الاهتمام بهذه الوظائف التي تأخذ جميعها ، كمعامل أول ، الكائن الذي تعمل عليه وثانيًا اسم السمة (في شكل سلسلة). ومع ذلك ، قد يكون من الملائم جدًا أحيانًا العمل مع السلاسل بدلاً من أسماء السمات. علاوة على ذلك ، هذا ما فعلناه للتو ، في إعادة تعريف طرق الوصول إلى السمات.

مرة أخرى ، إذا لم تكن المصلحة واضحة ، اترك هذه الوظائف جانبًا. يمكنك أن تجدهم لاحقًا.

طرق الحاوية (container)



سنبدأ العمل على ما يسمى الحمل الزائد للمشغل (operator overload) . هذه مجرد مسألة شرح لبايثون ما يجب فعله عند استخدام مشغل معين. سنرى هنا أربع طرق خاصة تدخل حيز التنفيذ عند العمل مع كائنات حاوية.

الوصول إلى عناصر الحاوية


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

الطرق الثلاثة الأولى التي نحن بصدد رؤيتها هي __getitem__، __setitem__و __delitem__ . يتم استخدامها على التوالي لتحديد ما يجب فعله عند كتابة:

  • objet[index]؛
  • objet[index] = valeur؛
  • del objet[index]؛
في هذا المثال ، سنرى فئة غلاف القاموس. فصول الالتفاف هي فصول تشبه الفئات الأخرى ، لكنها ليست كذلك. 

سنقوم بإنشاء فئة نسميها ZDict . سيكون لها سمة لا ينبغي لنا الوصول إليها من خارج الفئة ، قاموس نسميه _dictionnaire . عندما نقوم بإنشاء كائن من النوع ZDict ونريد القيام به objet[index]، سنفعل داخل الفئة self._dictionnaire[index] . في الواقع ، سوف تتظاهر فئتنا بأنه قاموس ، وسوف يتفاعل بنفس الطريقة ، لكنه لن يكون كذلك.


class ZDict:
    """ فئة غلاف القاموس """
    def __init__(self):
        """ لا تقبل فئتنا أي معلمات """
        self._dictionnaire = {}
    def __getitem__(self, index):
        """ objet[index]  تستدعي هذه الطريقة الخاصة عندما نكتب   
        قوم بإعادة التوجيه إلى self._dictionary [index] """
        
        return self._dictionnaire[index]
    def __setitem__(self, index, valeur):
        """ objet[index] = valeur تسمى هذه الطريقة عندما نكتب 
        نعيد التوجيه إلى self._dictionnaire[index] = valeur"""
        
        self._dictionnaire[index] = valeur
 
لديك مثال باستخدام كل الأساليب __getitem__و __setitem__التي أعتقد أنها واضحة جدا. ل __delitem__، وأعتقد أنه من الواضح جدا، إلا أنها تأخذ معلمة واحدة وهو مؤشر نريد حذفه. يمكنك توسيع هذا المثال بطرق أخرى رأيناها أعلاه ، بما في ذلك __repr__و __str__ . لا تتردد ، درب نفسك ، كل هذا يمكن أن يساعدك.

الطريقة الخاصة وراء الكلمة المفتاحية in


هناك طريقة رابعة تسمى __contains__، تُستخدم عندما نريد معرفة ما إذا كان الكائن في حاوية.

مثال كلاسيكي:


ma_liste = [1, 2, 3, 4, 5]
8 in ma_liste 
ma_liste.__contains__(8)
 
وبالتالي ، إذا كنت تريد أن تتمكن فئة الغلاف الخاصة بك من استخدام الكلمة الأساسية in كقائمة أو قاموس ، فيجب عليك إعادة تعريف هذه الطريقة __contains__التي تأخذ كمعامل ، بالإضافة self إلى الكائن الذي يهمنا. إذا كان الكائن في الحاوية ، يجب أن يعيد True؛ خلاف ذلك False .

اسمح لك بإعادة تعريف هذه الطريقة ، فلديك كل المؤشرات اللازمة.

اعرف حجم الحاوية


أخيرًا ، هناك طريقة خاصة __len__تسمى عندما نريد معرفة حجم كائن الحاوية ، وذلك بفضل الوظيفة len .

len(objet) يعادل objet.__len__() . لا تأخذ هذه الطريقة الخاصة أي معلمات وتُرجع الحجم كعدد صحيح. مرة أخرى ، سأدعك تجربها.

الطرق الرياضية



لهذا القسم، وسوف نستمر في رؤية طرق خاصة إثقال مشغلي الرياضيات ، مثل +، -، *وهلم جرا.

ما يجب أن تعرفه


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

هنا جسد الفئة ، احتفظ به في متناول اليد:


class Duree:
    """ فئة تحتوي على فترات كعدد من الدقائق
    وثواني """
    
    def __init__(self, min=0, sec=0):
        """ مُنشئ فئة """
        self.min = min # عدد الدقائق
        self.sec = sec # عدد الثواني
    def __str__(self):
        """ عرض أجمل قليلاً لأشياءنا """
        return "{0:02}:{1:02}".format(self.min, self.sec)
 
نحن ببساطة نحدد سمتين تحتويان على عدد الدقائق وعدد الثواني لدينا ، بالإضافة إلى طريقة لعرضها جميعًا بشكل أفضل قليلاً. إذا كنت تتساءل عن استخدام الطريقة format في الطريقة __str__ ، فاعلم فقط أن الهدف هو رؤية المدة في النموذج MM:SS ؛ لمزيد من المعلومات حول تنسيق السلاسل ، يمكنك الرجوع  إلى وثائق Python  .

لنقم بإنشاء أول كائن Duree نسميه d1 .


>>> d1 = Duree(3, 5)
>>> print(d1)
03:05
>>>
 
إذا حاولت القيام بذلك d1 + 4، على سبيل المثال ، فسوف تحصل على خطأ. لا تعرف Python كيفية إضافة نوع Duree و int . إنه لا يعرف حتى كيفية إضافة فترتين! لذا سنشرح له ذلك.

الطريقة الخاصة لإعادة التعريف هي __add__ . يأخذ كمعامل الكائن الذي نريد إضافته. إليك سطرين من التعليمات البرمجية يعيدان نفس الشيء:


d1 + 4
d1.__add__(4)
 
كما ترى ، عند استخدامك للرمز مثل +هذا ، فهي في الواقع طريقة __add__الكائن Duree التي تُستدعى. تأخذ كمعامل الكائن الذي تريد إضافته ، بغض النظر عن نوع الكائن المعني. ويجب أن تعيد شيئًا قابلاً للاستغلال ، وهنا سيكون من المنطقي أكثر أن تكون مدة جديدة.

إذا كنت بحاجة إلى القيام بإجراءات مختلفة بناءً على نوع الكائن المراد إضافته ، فاختبر النتيجة type(objet_a_ajouter) .


def __add__(self, objet_a_ajouter):
        """ الكائن المراد إضافته هو عدد صحيح ، عدد الثواني """
        nouvelle_duree = Duree()
        # سنقوم بنسخ الذات في الكائن الذي تم إنشاؤه للحصول على نفس المدة
        nouvelle_duree.min = self.min
        nouvelle_duree.sec = self.sec
        # نضيف المدة
        nouvelle_duree.sec += objet_a_ajouter
        # إذا كان عدد الثواني> = 60
        if nouvelle_duree.sec >= 60:
            nouvelle_duree.min += nouvelle_duree.sec // 60
            nouvelle_duree.sec = nouvelle_duree.sec % 60
        # نعيد المدة الجديدة
        return nouvelle_duree
 
خذ الوقت الكافي لفهم الآلية والحسابات الصغيرة للتأكد من أن لديك مدة ثابتة. أولاً ، نقوم بإنشاء مدة جديدة تعادل المدة الواردة في self . نزيدها بعدد الثواني التي نضيفها ونتأكد من أن الوقت ثابت (عدد الثواني لا يصل إلى 60). إذا كان الوقت غير ثابت ، فإننا نصححه. نعيد أخيرًا كائننا الجديد المعدل. إليك كودًا صغيرًا يوضح كيفية استخدام طريقتنا:


>>> d1 = Duree(12, 8)
>>> print(d1)
12:08
>>> d2 = d1 + 54 # d1 + 54 secondes
>>> print(d2)
13:02
>>>
 
للفهم أفضل ، استبدل  d2 = d1 + 54 بـ d2 = d1.__add__(54) : إنه نفس الشيء. يعمل هذا الاستبدال فقط على فهم الآلية بشكل كامل. وغني عن القول أن هذه الأساليب الخاصة لا يجب استدعاؤها مباشرة من خارج الفئة ، فلم يتم اختراع المشغلين من أجل لا شيء.

لاحظ أنه في نفس النموذج توجد الطرق:

  • __sub__ : المشغل الزائد -؛
  • __mul__ : المشغل الزائد *؛
  • __truediv__ : المشغل الزائد /؛
  • __floordiv__ : المشغل الزائد //(تقسيم صحيح) ؛
  • __mod__ : المشغل الزائد %(modulo) ؛
  • __pow__ : المشغل الزائد **(الطاقة) ؛
  • ...
هناك المزيد الذي يمكنك التحقق منه على موقع Python .
كل هذا يتوقف على المعنى

ربما تكون قد لاحظت ، ومن المنطقي أنك اتبعت توضيحي ، لكن الكتابة objet1 + objet2 ليست مثل الكتابة objet2 + objet1 إذا كان الموضوعان من نوعين مختلفين.

في الواقع ، وفقًا للحالة ، __add__يتم استدعاء طريقة واحد أو آخر من الكائنات.

هذا يعني أنه عندما نستخدم الفئة Duree ، إذا كتبناه d1 + 4 يعمل ، ولكن 4 + d1 لا يعمل . في الواقع ، int لا يعرف الفئة ماذا يفعل مع الكائن الخاص بك Duree .

ومع ذلك ، هناك مجموعة متنوعة من الطرق الخاصة للقيام بالمهمة __add__إذا كنت تكتب العملية بالعكس. يكفي أن تسبق اسم الأساليب الخاصة بامتداد r .


def __radd__(self, objet_a_ajouter):
تسمى هذه الطريقة إذا كتبنا 4 + كائن و """       	
         الكائن الأول (4 في هذا المثال) لا يعرف كيف يضيف
         الثاني. نحن فقط نعيد التوجيه إلى __add__ منذ ،
         هنا يعود الأمر إلى نفس الشيء: يجب أن يكون للعملية نفس النتيجة ،
         تم طرحه في اتجاه واحد أو آخر """        
        return self + objet_a_ajouter
 
الآن يمكننا أن نكتب 4 + d1، إنه نفس الشيء d1 + 4 .

لا تتردد في إعادة قراءة هذه الأمثلة إذا بدت غير واضحة لك.

المشغلين الآخرين


من الممكن أيضًا تحميل المشغلين بشكل زائد += ، -= إلخ. أسماء الطرق التي رأيناها مسبوقة هذه المرة بـ  i .

طريقة المثال __iadd__لفئتنا Duree :


def __iadd__(self, objet_a_ajouter):
        """ الكائن المراد إضافته هو عدد صحيح ، عدد الثواني """
        # self نحن نعمل مباشرة على     هذه المرة
        # نضيف المدة
        self.sec += objet_a_ajouter
        # إذا كان عدد الثواني> = 60
        if self.sec >= 60:
            self.min += self.sec // 60
            self.sec = self.sec % 60
        # نُعيد self
        return self
 
وفي الصور:

>>> d1 = Duree(8, 5)
>>> d1 += 128
>>> print(d1)
10:13
>>>
 
لا يسعني إلا أن أشجعك على إجراء بعض الاختبارات للتأكد من فهمك للآلية. لقد أعطيتك طريقة للقيام بذلك هنا من خلال التعليق عليها ، ولكن إذا لم تتدرب أو تحاول بنفسك ، فلن تفهم المنطق بالضرورة.

طرق المقارنة



أخيرًا ، سنرى عوامل مقارنة الحمل الزائد التي تعرفها لبعض الوقت الآن: ==، !=، <، >، <=، >=.

يتم استدعاء هذه الأساليب إذا حاولت مقارنة كائنين مع بعضهما البعض. كيف تعرف بايثون أن 3 أقل من 18؟ طريقة خاصة للفئة int تسمح بذلك ، مع التبسيط. لذلك إذا كنت تريد مقارنة الأوقات ، على سبيل المثال ، فسيتعين عليك إعادة تحديد بعض الطرق التي سأقدمها أدناه. يجب أن يأخذوا الكائن كمعامل للمقارنة به self، ويجب أن يعيد قيمة منطقية (  Trueأو False ) .

سأعطيك فقط جدولًا موجزًا ​​بسيطًا للطرق لإعادة تعريف مقارنة كائنين ببعضهما البعض:

المشغل أو العامل طريقة خاصة ملخص
== def __eq__(self, objet_a_comparer): عامل المساواة ( متساوي ) . يعيد True إذا self و objet_a_comparer على قدم المساواة، Falseعلى خلاف ذلك.
!= def __ne__(self, objet_a_comparer): يختلف عن ( لا يساوي ) . يُعيد True إذا self و objet_a_comparerمختلفة، Falseعلى خلاف ذلك.
> def __gt__(self, objet_a_comparer): اختبارات ما إذا كانت self أكبر من objet_a_comparer
>= def __ge__(self, objet_a_comparer): اختبارات ما إذا كانت self (أكبر أو يساوي ) ل objet_a_comparer .
< def __lt__(self, objet_a_comparer): اختبارات ما إذا كانت self أقل من objet_a_comparer .
<= def __le__(self, objet_a_comparer): اختبارات إذا كانت self أقل أو تساوي objet_a_comparer .
لاحظ أنه يتم استدعاء هذه الطرق الخاصة إذا كنت تريد ، على سبيل المثال ، فرز قائمة تحتوي على كائناتك.

اعلم أيضًا أنه إذا لم تتمكن Python من القيام بذلك objet1 < objet2، فستحاول العملية المعاكسة أيضًا objet2 >= objet1 . ينطبق هذا أيضًا على عوامل المقارنة الأخرى التي رأيناها للتو.

تعال ، سأعطيك مثالين على أي حال ، الأمر متروك لك لإعادة تعريف الطرق الأخرى المذكورة أعلاه:


def __eq__(self, autre_duree):
        """متساويان self و autre_duree يختبر اذا كانت """
        return self.sec == autre_duree.sec and self.min == autre_duree.min
def __gt__(self, autre_duree):
        """ self > autre_duree"""
        nb_sec1 = self.sec + self.min * 60
        nb_sec2 = autre_duree.sec + autre_duree.min * 60
        return nb_sec1 > nb_sec2
 
أعتقد أن هذه الأمثلة يجب أن تكون كافية لك.

طرق خاصة مفيدة لـ pickle



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

دعونا نأخذ حالة محددة ذات فائدة عملية مشكوك فيها.

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

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

الطريقة الخاصة__getstate__


يتم استدعاء الأسلوب__getstate__ عند إجراء تسلسل للكائن. عندما تريد تسجيل الكائن باستخدام الوحدة النمطية pickle ، __getstate__فسيتم استدعاؤه قبل التسجيل مباشرة.

إذا لم يتم تحديد طريقة __getstate__، pickle فسيتم حفظ قاموس سمات الكائن المراد حفظه. تذكرون ؟ إنه وارد في objet.__dict__ .

خلاف ذلك ،  pickle يحفظ القيمة التي تم إرجاعها بواسطة __getstate__(عادةً ما يكون قاموس سمات معدل) في الملف.

دعونا نرى كيفية كتابة مثالنا باستخدام __getstate__ :


class Temp:
    """ فئة تحتوي على عدة سمات ، بما في ذلك واحد مؤقت """
    
    def __init__(self):
        """منشئ الفئة"""
        self.attribut_1 = "une valeur"
        self.attribut_2 = "une autre valeur"
        self.attribut_temporaire = 5
   
    def __getstate__(self):
        """ تُرجع قاموس السمات لإجراء تسلسل """
        dict_attr = dict(self.__dict__)
        dict_attr["attribut_temporaire"] = 0
        return dict_attr
 
قبل العودة إلى الكود ، يمكنك رؤية التأثيرات. إذا حاولت حفظ هذا الكائن pickle ثم استعادته من الملف ، فستجد أن السمة attribut_temporaire موجودة 0، بغض النظر عن قيمتها الأصلية.

دعنا نرى كود __getstate__ . لا تأخذ الطريقة أي وسيطات (باستثناء self أنها طريقة مثيل) .

يحفظ قاموس السمات في متغير محلي dict_attr . يحتوي هذا القاموس على نفس محتوى self.__dict__ (قاموس سمات الكائن) . ومع ذلك ، فإنه يحتوي على مرجع مختلف. خلاف ذلك ، في السطر التالي ، عند التحرير attribut_temporaire ، سيتم تطبيق التغيير أيضًا على الكائن ، والذي نريد تجنبه.

لذلك ، في السطر التالي ، نقوم بتغيير قيمة السمة attribut_temporaire . لأن dict_attr و self.__dict__ ليس لديهم نفس المرجع، يتم تغيير السمة فقط في dict_attr والقاموس من self لم يتم تغييره.

أخيرا نعيد dict_attr . بدلاً من التسجيل في ملفنا self.__dict__، pickle غيرت السجلات قاموسنا dict_attr .

إذا لم يكن هذا واضحًا بما فيه الكفاية ، فأنا أشجعكم على الاختبار بأنفسكم ومحاولة تعديل الطريقة __getstate__والتلاعب self.__dict__ لفهم الكود تمامًا.

الطريقة__setstate__


على عكس __getstate__، __setstate__يتم استدعاء الطريقة عند إلغاء تسلسل الكائن. بشكل ملموس ، إذا قمت باستعادة كائن من ملف متسلسل ، __setstate__فسيتم استدعاؤه بعد استرداد قاموس السمات.

للتخطيط ، هذا هو التنفيذ الذي سنلاحظه وراءه unpickler.load() :

  1. كائن Unpickler يقرأ الملف.
  2. يسترد قاموس السمات. أذكرك أنه إذا لم يتم تحديد طريقة__getstate__ في صنفنا ، فإن هذا القاموس هو القاموس الموجود في السمة الخاصة __dict__للكائن في وقت تسلسله.
  3. يتم إرسال هذا القاموس المسترجع إلى الطريقة __setstate__إذا كان موجودًا. إذا لم يكن موجودًا ، فإن Python تعتبر أنه قاموس سمات الكائن المراد استرجاعه ، وبالتالي يكتب سمة __dict__الكائن عن طريق وضع هذا القاموس المسترجع هناك.
نفس المثال ولكن هذه المرة بالطريقة __setstate__ :

...
    def __setstate__(self, dict_attr):
        """ يتم استدعاء الأسلوب عند إلغاء تسلسل الكائن """
        dict_attr["attribut_temporaire"] = 0
        self.__dict__ = dict_attr
 
ما هو الفرق بين الطريقتين ؟

يمكن تحقيق الهدف الذي وضعناه لأنفسنا من خلال هاتين الطريقتين. إما أن تنفذ فئتنا طريقة __getstate__، أو أنها تنفذ طريقة __setstate__ .

في الحالة الأولى ، نقوم بتعديل قاموس السمات قبل التسلسل. قاموس السمات المحفوظة هو المعجم الذي قمنا بتعديله بقيمة السمة المؤقتة الخاصة بنا إليه 0 .

في الحالة الثانية ، يتم تعديل قاموس السمات بعد إلغاء التسلسل. يحتوي القاموس الذي نسترده على سمة attribut_temporaire ذات قيمة عشوائية (لا نعرفها) ولكن بعد استرداد الكائن الذي تم إنشاء مثيل له بالفعل (وقبل إرجاع إلغاء التسلسل!) ، قمنا بتعيين هذه القيمة على 0 .

هاتان وسيلتان مختلفتان ، وهما نفس الشيء هنا. الأمر متروك لك لاختيار أفضل طريقة وفقًا لاحتياجاتك (يمكن أن يكون كلاهما موجودًا في نفس الفئة إذا لزم الأمر).

مرة أخرى ، أشجعك على التجربة إذا لم يكن الأمر واضحًا تمامًا.

يمكنك حفظ شيء آخر غير القواميس في ملف


طريقتك __getstate__غير مطلوبة لإرجاع قاموس السمات. يمكنها إرجاع كائن آخر ، عددًا صحيحًا ، عددًا عشريًا ، ولكن في هذه الحالة __setstate__يجب أن توجد طريقة لمعرفة "ما يجب القيام به" مع الكائن المسجل. إذا لم يكن قاموس سمات ، فلا يمكن لبايثون التخمين!

مرة أخرى ، سأدعك تختبر إذا كنت مهتمًا.

أريد المزيد من القوة!


__getstate__و __setstate__هما أفضل الطرق المعروفة بناء على التسلسل الكائن. لكن هناك أخرى أكثر تعقيدًا.

إذا كنت مهتمًا ، فألق نظرة على PEP 307  .

باختصار


  • تسمح لك الطرق الخاصة بالتأثير على طريقة وصول Python إلى سمات مثيل وتتفاعل مع عوامل تشغيل أو تحويلات معينة.
  • الأساليب الخاصة كلها محاطة بشرطتين ( _).
  • تتحكم أساليب و __getattr__، في الوصول إلى سمات المثيل.__setattr____delattr__
  • و __getitem__، __setitem__و طرق __delitem__تجاوز الفهرسة ( []) .
  • الأساليب __add__، __sub__، __mul__ ... تفرط في العمليات الحسابية.
  • الأساليب __eq__، __ne__، __gt__... عوامل المقارنة الزائد.