أتقن نموذج الكائنات في Python: عقد __eq__/__hash__، والقيمة NotImplemented، ومديرو السياق، وتحديد الأنواع البنيوي عبر Protocol.
افتح هذا الدرس في Kodokonفي Python، يمرّ كل مُعامِل وكل تحويل وكل عبارة with عبر دالة خاصة: فإن a == b تستدعي a.__eq__(b)، وlen(x) تستدعي x.__len__(). واحرص على التمييز بين __repr__ - التمثيل الدقيق الموجَّه إلى المطوّر، الذي يظهر في المنقّح وداخل القوائم - و__str__، الموجَّهة إلى المستخدم النهائي. فإذا غابت __str__، تعود Python إلى __repr__: لذا نفّذ __repr__ أولًا دائمًا، ويُفضَّل في صيغة تتيح إعادة بناء الكائن.
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 (القيمة، ولا تخلط بينها وبين الاستثناء NotImplementedError) يُطلق بروتوكول التفاوض بين المُعامِلات: فتجرّب Python عندئذٍ العملية المعكوسة other.__eq__(self)، ولا تستنتج False إلا كملاذ أخير. أما إرجاع False مباشرة فيقطع طريق هذا التفاوض ويكسر المقارنات مع الأصناف الفرعية أو الأنواع الخارجية التي كانت في الواقع تعرف كيف تجيب.
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")إذا أرجعت __exit__ القيمة True، فإن الاستثناء الحالي يُكبَت - وهكذا تعمل contextlib.suppress بالضبط. ولتجنّب كتابة صنف كامل، يحوّل @contextlib.contextmanager مولّدًا إلى مدير سياق: فالكود الذي يسبق yield يلعب دور __enter__، والكود الذي يليه (ويُفضَّل داخل finally) يلعب دور __exit__. وركيزة أخرى من نموذج الكائنات: تُضفي أنواع Protocol من الوحدة typing طابعًا رسميًا على تنميط البطة (duck typing) - فالصنف متوافق إذا كشف عن الدوال الصحيحة، دون أيّ وراثة معلَنة.
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))__eq__، ماذا يجب أن تُرجِع عندما تواجه نوعًا لا يمكنك مقارنته به؟__eq__ إلى صنف دون أن تمسّ __hash__. ما النتيجة؟__exit__ القيمة True؟