ข้ามไปยังเนื้อหา

OOP และ Data Model

Python เป็นภาษา object-oriented เต็มรูปแบบ: integer, string, function, module — ทุกค่าเป็น object ที่มี identity, type และ value Class คือ blueprint; object (หรือ instance) คือสิ่งที่คุณสร้างจาก blueprint นั้นด้วย ClassName(...)

x = 42
print(type(x)) # <class 'int'>
print(id(x)) # unique memory address

Python เปิดให้เข้าถึง built-in operation เกือบทั้งหมดผ่าน dunder methods (method double-underscore อย่าง __init__, __repr__, __add__) เมื่อคุณเขียน a + b, Python เรียก a.__add__(b) เมื่อคุณเขียน len(obj), Python เรียก obj.__len__() เมื่อคุณเขียน repr(obj), Python เรียก obj.__repr__()

Protocol นี้เรียกว่า Python Data Model — มันทำให้ class ของคุณผสานเข้ากับ built-in syntax และ function ของ standard library ได้อย่างไร้รอยต่อ

class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
  • __init__ คือ initialiser — มันรันทันทีหลัง object ถูกสร้างและตั้งค่า instance attribute
  • __repr__ ให้ object มี string representation ที่เป็นมิตรกับ developer

Chain ไม่หยุดแค่ “object มี class” Class เองก็เป็น object และมี type ด้วย — type(Point) คือ <class 'type'> type คือ metaclass ที่เป็น built-in: type ของทุก class ธรรมดา รวมถึงตัวมันเอง (type(type) คือ type — chain จบตรงนั้น)

flowchart LR
p1["p1 (a Point instance)"] -->|"type(p1)"| Point["Point (the class)"]
Point -->|"type(Point)"| Type["type (the metaclass)"]
Type -->|"type(type)"| Type
Type chain ของทุก object จบที่ type ซึ่งเป็น type ของตัวเอง

นี่ไม่ใช่กรณีพิเศษของชื่อ built-in ตัวเดียว — มันคือวิธีที่ทุก class ใน Python ถูกสร้าง รวมถึง class ที่คุณเขียนเองด้วย Point.__mro__ คือ (Point, object): แม้ class ที่ไม่มี base ชัดเจนก็สืบทอดจาก object ในที่สุด ซึ่งเป็นรากของ class hierarchy ทุกตัว บทถัดไปในโมดูลนี้ใช้ chain นี้ตลอด: isinstance(x, C) เดินตามมัน super() เดินตาม MRO ที่มันสร้างสำหรับ base หลายตัว และ metaclass (เครื่องมือขั้นสูงที่แทบไม่จำเป็นต้องใช้) ก็แค่ class ที่ instance ของมันเป็น class เอง — subclass ของ type ขึ้นไปอีกหนึ่งชั้นจากที่โค้ดทั่วไปต้องไปถึง

บทเรียนหัวข้อ
1. Classesclass, __init__ vs __new__, shared mutable class attribute, MRO/diamond inheritance, isinstance vs type(), duck typing vs ABC/Protocol
2. Dunder methods__repr__/__str__, __eq__/__hash__, NotImplemented, __iter__/__next__, __call__, __getattr__/__getattribute__
3. Properties@property ในฐานะ descriptor, การเขียน descriptor เอง, cached_property, @classmethod ที่ return Self
4. Dataclasses@dataclass, field, frozen=True, slots=True, order=True, kw_only, dataclass เทียบกับ NamedTuple/TypedDict/pydantic
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
def __add__(self, other: "Point") -> "Point":
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1)
print(p1 + p2)
print(type(p1))
print(isinstance(p1, Point))
print(type(Point)) # the class itself is an instance of type
print(type(type(p1))) # same thing, spelled through the instance
print(type(type)) # type is its own metaclass -- the chain stops here
print(Point.__mro__)
class Base:
def __init__(self) -> None:
self.ready = True
print("Base.__init__ ran")
class Broken(Base):
def __init__(self, name: str) -> None:
self.name = name # forgot super().__init__() -- Base.__init__ never runs
b = Broken("widget")
print(b.name)
try:
print(b.ready)
except AttributeError as e:
print(f"AttributeError: {e}")
flowchart TD
Call["Point(1, 2)"] --> New["type.__call__ runs Point.__new__(Point) -- creates a bare instance"]
New --> Init["Point.__init__(instance, 1, 2) -- fills in the attributes"]
Init --> Ready["fully-constructed p1"]
การสร้าง instance: __new__ สร้างมันขึ้นมา __init__ เติมค่าให้

กลไกทุกอย่างที่โมดูลนี้ครอบคลุม — attribute lookup, MRO, descriptor, การสร้าง dataclass — เป็นความหลากหลายของแนวคิดเดียวกัน: class คือ namespace ของชื่อ instance คือ namespace ที่บางกว่าซ้อนอยู่ด้านบน และ Python มีลำดับที่ชัดเจน ตรวจสอบได้เสมอสำหรับการ resolve ชื่อกับทั้งสอง Point(1, 2) ไม่ใช่ magic: type.__call__ รัน Point.__new__ เพื่อสร้าง object เปล่าๆ แล้วรัน Point.__init__ เพื่อเติมค่า — การแบ่งสองขั้นตอนแบบเดียวกับที่บท Classes ใช้อธิบาย singleton และ immutable-type subclass ถามตัวเองต่อไปว่า “object อะไร type อะไร ลำดับไหน” ระหว่างอ่านโค้ดที่เต็มไปด้วย class แล้ว Data Model ที่เหลือจะไม่รู้สึกเหมือนรายการ dunder method ที่ไม่เกี่ยวข้องกันอีกต่อไป

สิ่งที่ได้ประโยชน์ต้นทุน
ทุกอย่างเป็น objectMental model เดียวที่ใช้ได้กับ int, function, class และ module”Object” ไม่ได้แปลว่า “instance ของ class ที่ฉันเขียน” อีกต่อไป — มันรวมสิ่งที่รู้สึกเหมือน syntax ด้วย
Data Model (dunder method)Class ของคุณผสานเข้ากับ +, len(), for, with และ built-in อื่นๆ ทุกตัวDunder ทุกตัวที่คุณ implement คือ contract ที่ต้องรักษาให้ถูกต้อง ไม่งั้น built-in syntax จะทำงานผิดแบบสับสน
Object model ที่ลึกและสม่ำเสมอ (instance → class → type)กฎเดียวกันอธิบาย classmethod, metaclass และ isinstance ได้ทั้งหมดชั้น metaclass เป็น complexity จริงที่มองข้ามได้ง่ายจนกว่าบางอย่าง (ORM, framework) จะใช้มันกับคุณ
  • มีแค่สิ่งที่คุณสร้างด้วย class เท่านั้นที่เป็น “object” Integer, string, function, module และ class เองล้วนเป็น object ทั้งหมด — type(42), type(print) และ type(int) ล้วน return class จริง
  • Class ต่างจาก object ที่มันสร้างอย่างพื้นฐาน Class ก็เป็น object เหมือนกัน — เป็น instance ของ type — นั่นคือเหตุผลที่ type(Point) เป็นคำถามที่มีความหมายและมีคำตอบจริง
  • ระบบ dunder method เป็นรายการตายตัวที่ใช้หรือไม่ใช้ก็ได้ มันเป็น protocol แบบเปิด — class ไหนก็ opt-in บางส่วนของมันได้ และ Python เรียกแค่ method ที่จำเป็นสำหรับ syntax ที่ใช้จริงเท่านั้น
  • isinstance, super() และการสร้าง dataclass เป็น feature ที่ไม่เกี่ยวข้องกันที่ต้องท่องจำแยกกัน ทั้งหมดเป็นผลจาก object model เดียวกัน: isinstance เดินตาม class hierarchy super() เดินตาม MRO และ @dataclass ก็แค่เขียน method ธรรมดาลงบน class ด้วยกฎเดียวกับที่ method ที่เขียนด้วยมือทุกตัวทำตาม

ก่อนเชื่อ class hierarchy ที่ generate มาแค่มองผ่านๆ ตรวจสิ่งเหล่านี้:

  • __init__ ของ subclass เรียก super().__init__(...) ไหม? การข้ามมันเป็น bug ที่พบบ่อยที่สุดตัวหนึ่งในโค้ดที่ generate มา — attribute ที่ parent ตั้งค่า (รวมถึงตัวที่ method อื่นพึ่งพา) จะไม่มีอยู่บน subclass เลย และ failure จะปรากฏทีหลังเป็น AttributeError ที่ห่างไกลจากจุดที่ขาดการเรียก
  • โค้ดเทียบ type ด้วย type(x) == SomeClass ในจุดที่ตั้งใจจะเช็คแบบ isinstance (ที่เคารพ subclass) ชัดเจนไหม?
  • มี object mutable ถูก assign เป็น class-level attribute ที่ method ไหนก็ตามแก้ในที่เดิมไหม? ทุก instance จะแชร์มัน
  • Comparison dunder (__eq__) ถูกกำหนดโดยไม่มี __hash__ ที่เข้าคู่กันไหม แล้ว class นั้นถูกเอาไปใช้ใน set หรือเป็น dict key ทีหลังไหม?
  • มี inheritance chain ที่ลึกในจุดที่ความสัมพันธ์แบบ has-a (composition) จะง่ายกว่าและเปราะบางน้อยกว่า “subclass อีกตัวก็พอ” ไหม?

🐛 หา bug ในโค้ดนี้

class Connection:
def __init__(self, host: str) -> None:
self.host = host
self.is_open = True
class LoggingConnection(Connection):
def __init__(self, host: str, log_prefix: str) -> None:
self.log_prefix = log_prefix # forgot super().__init__(host)
conn = LoggingConnection("db.example.com", "[DB]")
print(conn.log_prefix)
print(conn.is_open)
เฉลย

LoggingConnection.__init__ ไม่เคยเรียก super().__init__(host) เลย ดังนั้น Connection.__init__ จึงไม่เคยรัน — self.host และ self.is_open ไม่เคยถูกตั้งค่า conn.log_prefix พิมพ์ได้ปกติ แต่ conn.is_open raise AttributeError: 'LoggingConnection' object has no attribute 'is_open' ยืนยันแล้วบน 3.14 นี่คือ bug ที่พบบ่อยที่สุดตัวหนึ่งในโมดูลนี้ที่เชื่อมหลายบทเข้าด้วยกัน: มันดูเหมือน cooperative-super() chain จากบท Classes ทุกประการ ยกเว้นการเรียกหายไปทั้งหมด ทำให้ chain ไม่เคยเริ่มต้น แก้โดยเพิ่ม super().__init__(host) เป็นบรรทัดแรกของ LoggingConnection.__init__

Dunder method ไหนรันเมื่อคุณเขียน obj = MyClass(1, 2)?
Python เรียกอะไรภายในเมื่อคุณเขียน len(my_list)?
คำที่ใช้เรียกระบบ dunder method ที่ผสาน class เข้ากับ Python syntax คืออะไร?
ข้อความไหนเกี่ยวกับ Python object เป็นจริง?
โค้ดนี้พิมพ์อะไร?
class Point:
    pass

print(type(Point))
print(type(type(Point)))
บรรทัดสุดท้ายเกิดอะไรขึ้น?
class Base:
    def __init__(self):
        self.ready = True

class Broken(Base):
    def __init__(self, name):
        self.name = name

b = Broken("x")
print(b.ready)
Point.__mro__ return อะไรสำหรับ class ที่กำหนดเป็น class Point: pass ที่ไม่มี base ชัดเจน?