Kodokon kodokon.com

البروتوكولات والدوال الخاصة: __repr__، __eq__، مديرو السياق

أتقن نموذج الكائنات في Python: عقد __eq__/__hash__، والقيمة NotImplemented، ومديرو السياق، وتحديد الأنواع البنيوي عبر Protocol.

9 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

في Python، يمرّ كل مُعامِل وكل تحويل وكل عبارة with عبر دالة خاصة: فإن a == b تستدعي a.__eq__(b)، وlen(x) تستدعي x.__len__(). واحرص على التمييز بين __repr__ - التمثيل الدقيق الموجَّه إلى المطوّر، الذي يظهر في المنقّح وداخل القوائم - و__str__، الموجَّهة إلى المستخدم النهائي. فإذا غابت __str__، تعود Python إلى __repr__: لذا نفّذ __repr__ أولًا دائمًا، ويُفضَّل في صيغة تتيح إعادة بناء الكائن.

PYTHON
class Money:
    def __init__(self, amount: int, currency: str):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount!r}, {self.currency!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (
            other.amount, other.currency)

print(Money(10, "EUR") == Money(10, "EUR"))
print(Money(10, "EUR") == "10 EUR")
المقارنة بنوع غير معروف تُرجِع NotImplemented، لا False أبدًا.

إن إرجاع NotImplemented (القيمة، ولا تخلط بينها وبين الاستثناء NotImplementedError) يُطلق بروتوكول التفاوض بين المُعامِلات: فتجرّب Python عندئذٍ العملية المعكوسة other.__eq__(self)، ولا تستنتج False إلا كملاذ أخير. أما إرجاع False مباشرة فيقطع طريق هذا التفاوض ويكسر المقارنات مع الأصناف الفرعية أو الأنواع الخارجية التي كانت في الواقع تعرف كيف تجيب.

PYTHON
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb):
        stop = time.perf_counter()
        self.elapsed = stop - self.start
        return False

with Timer() as timer:
    sum(range(1_000_000))
print(f"{timer.elapsed:.4f} s")
إرجاع False يترك أيّ استثناء ينتشر بشكل طبيعي.

إذا أرجعت __exit__ القيمة True، فإن الاستثناء الحالي يُكبَت - وهكذا تعمل contextlib.suppress بالضبط. ولتجنّب كتابة صنف كامل، يحوّل @contextlib.contextmanager مولّدًا إلى مدير سياق: فالكود الذي يسبق yield يلعب دور __enter__، والكود الذي يليه (ويُفضَّل داخل finally) يلعب دور __exit__. وركيزة أخرى من نموذج الكائنات: تُضفي أنواع Protocol من الوحدة typing طابعًا رسميًا على تنميط البطة (duck typing) - فالصنف متوافق إذا كشف عن الدوال الصحيحة، دون أيّ وراثة معلَنة.

PYTHON
from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

class Session:
    def close(self) -> None:
        print("session closed")

def shutdown(resource: Closable) -> None:
    resource.close()

shutdown(Session())
print(isinstance(Session(), Closable))
الصنف Session لا يُعلن أيّ وراثة: فالمطابقة بنيوية.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. في __eq__، ماذا يجب أن تُرجِع عندما تواجه نوعًا لا يمكنك مقارنته به؟
    • القيمة False، للإشارة بوضوح إلى عدم التساوي
    • إطلاق الاستثناء NotImplementedError
    • القيمة NotImplemented، لتدع Python تجرّب العملية المعكوسة للمُعامَل الآخر
    • القيمة None، التي ستحوّلها Python إلى False
  2. تضيف __eq__ إلى صنف دون أن تمسّ __hash__. ما النتيجة؟
    • لا شيء: تبقى __hash__ موروثة من object
    • تُضبط __hash__ على None: فلا يمكن بعد ذلك استخدام المثيلات كمفاتيح قواميس أو كعناصر في مجموعات
    • تصبح التجزئة تلقائيًا تجزئة الصفّ (tuple) المكوَّن من الخصائص
  3. ماذا يعني أن تُرجِع الدالة __exit__ القيمة True؟
    • أن كتلة with انتهت دون خطأ
    • أن مدير السياق قابل لإعادة الاستخدام
    • أن Python ستُعيد تنفيذ كتلة with مرة ثانية
    • أن الاستثناء الحالي يُكبَت ويستأنف التنفيذ بعد كتلة with