เชี่ยวชาญโมเดลอ็อบเจกต์ของ Python: สัญญาระหว่าง __eq__/__hash__, ค่า NotImplemented, context manager และการใส่ชนิดข้อมูลเชิงโครงสร้างด้วย 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 (ตัวค่า อย่าสับสนกับ exception ชื่อ 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 exception ปัจจุบันจะถูก กลบทิ้ง - นั่นคือวิธีที่ contextlib.suppress ทำงานเป๊ะ ๆ หากไม่อยากเขียนคลาสเต็มรูปแบบ @contextlib.contextmanager จะเปลี่ยน generator ให้เป็น context manager โค้ดก่อน 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 หมายความว่าอะไร?