Kodokon kodokon.com

โปรโตคอลและเมท็อดพิเศษ: __repr__, __eq__, context manager

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

ถ้า __exit__ คืน True exception ปัจจุบันจะถูก กลบทิ้ง - นั่นคือวิธีที่ contextlib.suppress ทำงานเป๊ะ ๆ หากไม่อยากเขียนคลาสเต็มรูปแบบ @contextlib.contextmanager จะเปลี่ยน generator ให้เป็น context manager โค้ดก่อน 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 อินสแตนซ์ใช้เป็นคีย์ของดิกต์หรือสมาชิกของเซตไม่ได้อีกต่อไป
    • แฮชกลายเป็นแฮชของทูเพิลของแอตทริบิวต์โดยอัตโนมัติ
  3. เมท็อด __exit__ ที่คืน True หมายความว่าอะไร?
    • บล็อก with จบลงโดยไม่มีข้อผิดพลาด
    • context manager นำกลับมาใช้ซ้ำได้
    • Python จะรันบล็อก with ซ้ำอีกครั้งเป็นครั้งที่สอง
    • exception ปัจจุบันถูกกลบทิ้ง และการทำงานดำเนินต่อหลังบล็อก with