Giao diện
Descriptors & Properties Advanced
Descriptors = Bí mật đằng sau @property, @classmethod, @staticmethod
Learning Outcomes
Sau khi hoàn thành trang này, bạn sẽ:
- ✅ Hiểu Descriptor Protocol (
__get__,__set__,__delete__) - ✅ Phân biệt Data Descriptors vs Non-Data Descriptors
- ✅ Hiểu cách
@propertyhoạt động bên trong - ✅ Implement lazy attributes và cached properties
- ✅ Tạo custom descriptors cho validation và logging
- ✅ Tránh các production pitfalls với descriptors
Descriptor Protocol là gì?
Descriptor là object implement ít nhất một trong các methods: __get__, __set__, __delete__. Khi attribute lookup xảy ra, Python gọi các methods này thay vì trả về object trực tiếp.
python
class Descriptor:
"""Minimal descriptor example."""
def __get__(self, obj, objtype=None):
"""Được gọi khi: instance.attr hoặc Class.attr"""
print(f"__get__ called: obj={obj}, objtype={objtype}")
return "value from descriptor"
def __set__(self, obj, value):
"""Được gọi khi: instance.attr = value"""
print(f"__set__ called: obj={obj}, value={value}")
def __delete__(self, obj):
"""Được gọi khi: del instance.attr"""
print(f"__delete__ called: obj={obj}")
class MyClass:
attr = Descriptor() # Descriptor được gán như class attribute
# Sử dụng
obj = MyClass()
obj.attr # __get__ called: obj=<MyClass>, objtype=<class 'MyClass'>
obj.attr = 42 # __set__ called: obj=<MyClass>, value=42
del obj.attr # __delete__ called: obj=<MyClass>
MyClass.attr # __get__ called: obj=None, objtype=<class 'MyClass'>Data vs Non-Data Descriptors
| Loại | Methods | Ưu tiên |
|---|---|---|
| Data Descriptor | __get__ + __set__ (hoặc __delete__) | Cao nhất |
| Non-Data Descriptor | Chỉ __get__ | Thấp hơn instance __dict__ |
Attribute Lookup Order
python
# Thứ tự tìm kiếm khi truy cập obj.attr:
# 1. Data descriptor trong class (và MRO)
# 2. Instance __dict__
# 3. Non-data descriptor trong class (và MRO)
# 4. Class __dict__
# 5. __getattr__ (nếu có)
class DataDescriptor:
"""Data descriptor - có cả __get__ và __set__."""
def __get__(self, obj, objtype=None):
return "from data descriptor"
def __set__(self, obj, value):
pass # Có __set__ → Data descriptor
class NonDataDescriptor:
"""Non-data descriptor - chỉ có __get__."""
def __get__(self, obj, objtype=None):
return "from non-data descriptor"
class Demo:
data = DataDescriptor()
non_data = NonDataDescriptor()
obj = Demo()
obj.__dict__['data'] = "from instance"
obj.__dict__['non_data'] = "from instance"
print(obj.data) # "from data descriptor" (data descriptor wins!)
print(obj.non_data) # "from instance" (instance __dict__ wins!)@property Internals
@property là một data descriptor được implement sẵn trong Python.
Cách @property hoạt động
python
# Khi bạn viết:
class User:
@property
def name(self):
return self._name
# Python thực sự làm:
class User:
def name(self):
return self._name
name = property(name) # property là descriptor!Implement property từ đầu
python
class MyProperty:
"""Custom implementation của @property."""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc or (fget.__doc__ if fget else None)
def __get__(self, obj, objtype=None):
if obj is None:
return self # Truy cập từ class
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def setter(self, fset):
"""Decorator để thêm setter."""
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
"""Decorator để thêm deleter."""
return type(self)(self.fget, self.fset, fdel, self.__doc__)
# Sử dụng giống hệt @property
class User:
def __init__(self, name: str):
self._name = name
@MyProperty
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
user = User("HPN")
print(user.name) # "HPN"
user.name = "Tom"
print(user.name) # "Tom"Lazy Attribute Patterns
Pattern 1: Lazy Loading với Descriptor
python
class LazyProperty:
"""Descriptor tính toán một lần, sau đó cache vào instance."""
def __init__(self, func):
self.func = func
self.attr_name = None
def __set_name__(self, owner, name):
"""Python 3.6+: Được gọi khi descriptor được gán vào class."""
self.attr_name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Tính toán và cache vào instance __dict__
value = self.func(obj)
obj.__dict__[self.attr_name] = value # Bypass descriptor lần sau
return value
class DataProcessor:
def __init__(self, data: list):
self.data = data
@LazyProperty
def expensive_result(self) -> float:
"""Tính toán tốn kém - chỉ chạy một lần."""
print("Computing expensive result...")
return sum(x ** 2 for x in self.data)
processor = DataProcessor([1, 2, 3, 4, 5])
print(processor.expensive_result) # Computing... → 55
print(processor.expensive_result) # 55 (không tính lại!)
print('expensive_result' in processor.__dict__) # True (đã cache)Pattern 2: functools.cached_property (Python 3.8+)
python
from functools import cached_property
class DataProcessor:
def __init__(self, data: list):
self.data = data
@cached_property
def expensive_result(self) -> float:
"""Built-in lazy property."""
print("Computing...")
return sum(x ** 2 for x in self.data)
# Hoạt động giống LazyProperty ở trênPattern 3: Resettable Lazy Property
python
class ResettableLazyProperty:
"""Lazy property có thể reset để tính lại."""
def __init__(self, func):
self.func = func
self.attr_name = None
def __set_name__(self, owner, name):
self.attr_name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.attr_name not in obj.__dict__:
obj.__dict__[self.attr_name] = self.func(obj)
return obj.__dict__[self.attr_name]
def __delete__(self, obj):
"""Reset cache khi del."""
obj.__dict__.pop(self.attr_name, None)
class Config:
def __init__(self, path: str):
self.path = path
@ResettableLazyProperty
def settings(self) -> dict:
print(f"Loading from {self.path}...")
return {"loaded": True}
config = Config("config.yaml")
print(config.settings) # Loading... → {'loaded': True}
print(config.settings) # {'loaded': True} (cached)
del config.settings # Reset cache
print(config.settings) # Loading... (tính lại)Validation Descriptor
python
class Validated:
"""Descriptor với validation."""
def __init__(self, validator, default=None):
self.validator = validator
self.default = default
self.attr_name = None
def __set_name__(self, owner, name):
self.attr_name = f"_validated_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.attr_name, self.default)
def __set__(self, obj, value):
validated = self.validator(value)
setattr(obj, self.attr_name, validated)
# Validators
def positive_int(value):
value = int(value)
if value <= 0:
raise ValueError(f"Must be positive, got {value}")
return value
def non_empty_string(value):
value = str(value).strip()
if not value:
raise ValueError("Cannot be empty")
return value
def email_validator(value):
value = str(value).lower().strip()
if "@" not in value:
raise ValueError(f"Invalid email: {value}")
return value
class User:
name = Validated(non_empty_string)
age = Validated(positive_int, default=0)
email = Validated(email_validator)
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
# Sử dụng
user = User("HPN", 28, "HPN@Example.com")
print(user.name) # "HPN"
print(user.email) # "hpn@example.com" (normalized)
user.age = -5 # ValueError: Must be positive
user.name = " " # ValueError: Cannot be emptyType-Checked Descriptor
python
from typing import Any, Type
class TypeChecked:
"""Descriptor với type checking."""
def __init__(self, expected_type: Type, default: Any = None):
self.expected_type = expected_type
self.default = default
self.attr_name = None
def __set_name__(self, owner, name):
self.attr_name = f"_typed_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.attr_name, self.default)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.attr_name}: expected {self.expected_type.__name__}, "
f"got {type(value).__name__}"
)
setattr(obj, self.attr_name, value)
class Config:
host = TypeChecked(str, "localhost")
port = TypeChecked(int, 8080)
debug = TypeChecked(bool, False)
def __init__(self, host: str = "localhost", port: int = 8080):
self.host = host
self.port = port
config = Config("0.0.0.0", 3000)
config.port = "8080" # TypeError: expected int, got str__set_name__ Hook (Python 3.6+)
python
class Descriptor:
def __set_name__(self, owner, name):
"""
Được gọi tự động khi descriptor được gán vào class.
Args:
owner: Class chứa descriptor
name: Tên attribute được gán
"""
print(f"Descriptor assigned to {owner.__name__}.{name}")
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
class MyClass:
x = Descriptor() # Output: Descriptor assigned to MyClass.x
y = Descriptor() # Output: Descriptor assigned to MyClass.yProduction Pitfalls 🚨
Pitfall 1: Descriptor không hoạt động trên instance
python
# ❌ BUG: Gán descriptor vào instance không hoạt động
class Validator:
def __get__(self, obj, objtype=None):
return "validated"
def __set__(self, obj, value):
print(f"Validating: {value}")
class User:
pass
user = User()
user.name = Validator() # Gán vào instance, không phải class!
print(user.name) # <Validator object> - không gọi __get__!
# ✅ FIX: Gán vào class
class User:
name = Validator() # Gán vào class
user = User()
user.name = "HPN" # Validating: HPNPitfall 2: Non-data descriptor bị override
python
# ❌ BUG: Non-data descriptor bị instance __dict__ override
class Method:
def __get__(self, obj, objtype=None):
return lambda: "method called"
class MyClass:
action = Method()
obj = MyClass()
print(obj.action()) # "method called"
obj.__dict__['action'] = "overridden"
print(obj.action) # "overridden" - descriptor bị bypass!
# ✅ FIX: Dùng data descriptor (thêm __set__)
class Method:
def __get__(self, obj, objtype=None):
return lambda: "method called"
def __set__(self, obj, value):
raise AttributeError("Cannot override method")Pitfall 3: Quên xử lý class-level access
python
# ❌ BUG: Crash khi truy cập từ class
class Descriptor:
def __get__(self, obj, objtype=None):
return obj.value # obj là None khi truy cập từ class!
class MyClass:
attr = Descriptor()
MyClass.attr # AttributeError: 'NoneType' has no attribute 'value'
# ✅ FIX: Kiểm tra obj is None
class Descriptor:
def __get__(self, obj, objtype=None):
if obj is None:
return self # Trả về descriptor khi truy cập từ class
return obj.valuePitfall 4: Shared state giữa instances
python
# ❌ BUG: Tất cả instances share cùng data
class Descriptor:
def __init__(self):
self.value = None # Shared giữa tất cả instances!
def __get__(self, obj, objtype=None):
return self.value
def __set__(self, obj, value):
self.value = value
class User:
name = Descriptor()
u1 = User()
u2 = User()
u1.name = "Alice"
print(u2.name) # "Alice" - Oops! Shared state!
# ✅ FIX: Lưu data vào instance
class Descriptor:
def __set_name__(self, owner, name):
self.attr_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.attr_name, None)
def __set__(self, obj, value):
setattr(obj, self.attr_name, value) # Lưu vào instance!Pitfall 5: cached_property với mutable objects
python
from functools import cached_property
# ❌ BUG: Mutable object được cache
class DataLoader:
@cached_property
def items(self) -> list:
return [] # Trả về list rỗng
loader = DataLoader()
loader.items.append(1)
loader.items.append(2)
print(loader.items) # [1, 2] - Mutated cached value!
# ✅ FIX: Trả về copy hoặc immutable
class DataLoader:
@cached_property
def items(self) -> tuple:
return tuple(self._load_items()) # Immutable
def _load_items(self):
return [1, 2, 3]Bảng Tóm tắt
python
# === DESCRIPTOR PROTOCOL ===
class Descriptor:
def __get__(self, obj, objtype=None):
"""obj.attr hoặc Class.attr"""
pass
def __set__(self, obj, value):
"""obj.attr = value"""
pass
def __delete__(self, obj):
"""del obj.attr"""
pass
def __set_name__(self, owner, name):
"""Được gọi khi gán vào class (Python 3.6+)"""
pass
# === DATA vs NON-DATA ===
# Data Descriptor: có __set__ hoặc __delete__ → ưu tiên cao
# Non-Data Descriptor: chỉ có __get__ → instance __dict__ có thể override
# === LAZY PROPERTY ===
from functools import cached_property
@cached_property
def expensive(self):
return compute_once()
# === VALIDATION PATTERN ===
class Validated:
def __set_name__(self, owner, name):
self.name = f"_{name}"
def __get__(self, obj, objtype=None):
return getattr(obj, self.name, None) if obj else self
def __set__(self, obj, value):
validated = self.validate(value)
setattr(obj, self.name, validated)