OOP และ Data Model
ทุกอย่างใน Python เป็น object
หัวข้อที่มีชื่อว่า “ทุกอย่างใน Python เป็น object”Python เป็นภาษา object-oriented เต็มรูปแบบ: integer, string, function, module — ทุกค่าเป็น object ที่มี identity, type และ value
Class คือ blueprint; object (หรือ instance) คือสิ่งที่คุณสร้างจาก blueprint นั้นด้วย ClassName(...)
x = 42print(type(x)) # <class 'int'>print(id(x)) # unique memory addressSpecial-method protocol
หัวข้อที่มีชื่อว่า “Special-method protocol”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 ที่เรียบง่ายที่สุด
หัวข้อที่มีชื่อว่า “Class ที่เรียบง่ายที่สุด”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
Class ก็เป็น object เหมือนกัน: instance → class → type
หัวข้อที่มีชื่อว่า “Class ก็เป็น object เหมือนกัน: instance → class → type”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
นี่ไม่ใช่กรณีพิเศษของชื่อ 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. Classes | class, __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 |
Demo รันได้จริง
หัวข้อที่มีชื่อว่า “Demo รันได้จริง”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 typeprint(type(type(p1))) # same thing, spelled through the instanceprint(type(type)) # type is its own metaclass -- the chain stops hereprint(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}")Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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"]
กลไกทุกอย่างที่โมดูลนี้ครอบคลุม — 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 ที่ไม่เกี่ยวข้องกันอีกต่อไป
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| ทุกอย่างเป็น object | Mental 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 hierarchysuper()เดินตาม MRO และ@dataclassก็แค่เขียน method ธรรมดาลงบน class ด้วยกฎเดียวกับที่ method ที่เขียนด้วยมือทุกตัวทำตาม
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อ 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__