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

Metaclasses & Descriptors

descriptor กับ metaclass คือสองฟีเจอร์ระดับ runtime ที่ทรงพลังที่สุดของ Python ฝั่ง TypeScript มี decorator อยู่ (stage-3, ยัง experimental) แต่ทำได้จำกัดกว่ามาก

descriptor protocol และระบบ metaclass ของ Python ทำงานในชั้นที่ลึกกว่านั้น คือเข้าไปคุมว่าการเข้าถึง attribute และการสร้าง class เกิดขึ้นอย่างไรตอน runtime เข้าใจสองเรื่องนี้แล้วคุณจะเห็นภาพทันทีว่า @property, @dataclass และ ORM อย่าง SQLAlchemy ทำงานเบื้องหลังอย่างไร

descriptor คือออบเจ็กต์ใดก็ตามที่กำหนด __get__, __set__, หรือ __delete__ เมื่อ descriptor instance ถูก assign เป็น class attribute Python จะเรียก methods เหล่านี้แทนการ lookup ใน dictionary ทุกครั้งที่คุณเข้าถึง attribute บน instance

TypeScript
// TypeScript — closest analogy: getter/setter
class Temperature {
private _celsius: number;
constructor(celsius: number) {
this._celsius = celsius;
}
get fahrenheit(): number {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
const t = new Temperature(100);
console.log(t.fahrenheit); // 212
t.fahrenheit = 32;
console.log(t.fahrenheit); // 32
Python
# Python — descriptor protocol (reusable across classes)
class Validated:
"""Descriptor: ensures field is numeric."""
def __set_name__(self, owner, name):
self._name = name # เรียกตอน class creation
def __get__(self, obj, objtype=None):
if obj is None:
return self # class-level access
return getattr(obj, f"_{self._name}", None)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError(
f"{self._name} must be numeric, "
f"got {type(value).__name__}"
)
setattr(obj, f"_{self._name}", value)
class Product:
price = Validated() # descriptor instance เป็น class attr
quantity = Validated()
def __init__(self, price: float, quantity: int) -> None:
self.price = price
self.quantity = quantity
def total(self) -> float:
return self.price * self.quantity
item = Product(9.99, 3)
print(item.total()) # 29.97
item.price = "free" # TypeError: price must be numeric

ข้อสังเกตสำคัญ: descriptor Validated สามารถ reuse กับคลาสต่างๆ และ attributes ต่างๆ ได้จำนวนมาก getter/setter ของ TypeScript เป็นแบบ per-class และ per-property — คุณไม่สามารถแชร์ definition เดียวได้

Python เรียก __set_name__ ตอนสร้าง class (จังหวะที่ class body รัน) ทำให้ descriptor รู้ชื่อ attribute ที่ตัวเองถูก assign ไปให้ นี่คือเหตุผลที่คุณไม่ต้องเขียน price = Validated("price") เพราะ Python ใส่ชื่อให้เอง

class Validated:
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, f"_{self._name}", None)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError(
f"{self._name} must be numeric, got {type(value).__name__}"
)
setattr(obj, f"_{self._name}", value)
class Product:
price = Validated()
quantity = Validated()
def __init__(self, price: float, quantity: int) -> None:
self.price = price
self.quantity = quantity
def total(self) -> float:
return self.price * self.quantity
item = Product(9.99, 3)
print(f"Total: {item.total()}")
item.price = 19.99
print(f"Updated total: {item.total()}")
try:
item.price = "free"
except TypeError as e:
print(f"Caught: {e}")
อะไรทำให้ `__get__` ของ descriptor ถูกเรียก?
`__set_name__` รับ arguments อะไร?
metaclass เริ่มต้นสำหรับทุกคลาส Python คืออะไร?
ใน `__get__` ของ descriptor เมื่อ `obj is None` ควร return อะไร?