Giao diện
Magic Methods Quan trọng
Dunder Methods = Double Underscore = Bí mật của Python
Learning Outcomes
Sau khi hoàn thành trang này, bạn sẽ:
- ✅ Hiểu và implement các magic methods cơ bản (
__str__,__repr__,__eq__,__hash__) - ✅ Tạo container classes với
__len__,__getitem__,__iter__ - ✅ Sử dụng
__slots__để tối ưu memory cho classes có nhiều instances - ✅ Implement
__init_subclass__cho plugin systems và validation - ✅ Tạo generic classes với
__class_getitem__và Python 3.12 type syntax - ✅ Tránh các production pitfalls phổ biến với magic methods
Magic Methods là gì?
Magic Methods (hay Dunder Methods) là các phương thức đặc biệt với format __ten__. Python tự động gọi chúng trong các tình huống cụ thể.
python
class User:
def __init__(self, ten): # Được gọi bởi: User("HPN")
self.ten = ten
def __str__(self): # Được gọi bởi: str(user), print(user)
return self.ten
def __len__(self): # Được gọi bởi: len(user)
return len(self.ten)
user = User("HPN")
print(user) # Gọi __str__ → "HPN"
len(user) # Gọi __len__ → 5__str__ vs __repr__
Quy tắc Vàng
| Phương thức | Mục đích | Đối tượng |
|---|---|---|
__str__ | Dễ đọc | Người dùng cuối |
__repr__ | Rõ ràng, debug | Lập trình viên |
python
from datetime import datetime
class GiaoDich:
def __init__(self, so_tien: float, tien_te: str = "VND"):
self.so_tien = so_tien
self.tien_te = tien_te
self.thoi_gian = datetime.now()
def __str__(self) -> str:
"""Cho người dùng: Gọn gàng, dễ đọc."""
return f"{self.so_tien:,.0f} {self.tien_te}"
def __repr__(self) -> str:
"""Cho dev: Chi tiết, có thể tái tạo."""
return (
f"GiaoDich(so_tien={self.so_tien!r}, "
f"tien_te={self.tien_te!r}, "
f"thoi_gian={self.thoi_gian!r})"
)
gd = GiaoDich(99990000)
# Cho người dùng
print(gd) # 99,990,000 VND
str(gd) # "99,990,000 VND"
# Cho lập trình viên
repr(gd) # GiaoDich(so_tien=99990000, tien_te='VND', thoi_gian=...)
# Trong collections, __repr__ được dùng
[gd] # [GiaoDich(so_tien=99990000, ...)]💡 MẸO CỦA HPN
Luôn implement __repr__. Nếu chỉ implement một cái, hãy implement __repr__ — Python sẽ dùng nó làm fallback cho __str__.
__eq__ - So sánh Object
Mặc định, == so sánh identity (địa chỉ bộ nhớ), không phải giá trị.
python
class Diem:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
# Không có __eq__
d1 = Diem(1, 2)
d2 = Diem(1, 2)
print(d1 == d2) # False! (khác object trong bộ nhớ)Implement __eq__
python
class Diem:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, Diem):
return NotImplemented # Để Python xử lý
return self.x == other.x and self.y == other.y
def __hash__(self) -> int:
"""Cần thiết để dùng trong sets/dicts."""
return hash((self.x, self.y))
# Có __eq__
d1 = Diem(1, 2)
d2 = Diem(1, 2)
print(d1 == d2) # True!
# Có thể dùng trong sets (cần __hash__)
diem_set = {d1, d2} # Chỉ 1 điểm (chúng bằng nhau)Các Phương thức So sánh
python
from functools import total_ordering
@total_ordering # Tự động generate các phương thức so sánh còn thiếu
class PhienBan:
def __init__(self, chinh: int, phu: int):
self.chinh = chinh
self.phu = phu
def __eq__(self, other: object) -> bool:
if not isinstance(other, PhienBan):
return NotImplemented
return (self.chinh, self.phu) == (other.chinh, other.phu)
def __lt__(self, other: "PhienBan") -> bool:
"""Nhỏ hơn."""
return (self.chinh, self.phu) < (other.chinh, other.phu)
# Giờ tất cả phép so sánh đều hoạt động!
v1 = PhienBan(1, 0)
v2 = PhienBan(2, 0)
v3 = PhienBan(1, 5)
v1 < v2 # True
v1 <= v3 # True (@total_ordering tự generate)
v2 > v1 # True (@total_ordering tự generate)__len__ & __getitem__ - Container Protocol
Làm cho class của bạn hoạt động như list hoặc sequence.
python
class Playlist:
def __init__(self, ten: str):
self.ten = ten
self._bai_hat: list[str] = []
def them(self, bai: str) -> None:
self._bai_hat.append(bai)
def __len__(self) -> int:
"""len(playlist) → số bài hát."""
return len(self._bai_hat)
def __getitem__(self, index: int) -> str:
"""playlist[0] → bài đầu tiên."""
return self._bai_hat[index]
def __iter__(self):
"""for bai in playlist: ..."""
return iter(self._bai_hat)
def __contains__(self, bai: str) -> bool:
"""'bai' in playlist → True/False."""
return bai in self._bai_hat
# Sử dụng
playlist = Playlist("Nhạc Code")
playlist.them("Lo-Fi Beat 1")
playlist.them("Synthwave Track")
playlist.them("Ambient Mix")
len(playlist) # 3
playlist[0] # "Lo-Fi Beat 1"
playlist[-1] # "Ambient Mix"
"Synthwave Track" in playlist # True
for bai in playlist: # Duyệt hoạt động!
print(bai)Hỗ trợ Slicing
python
class DataBuffer:
def __init__(self, du_lieu: list):
self._du_lieu = du_lieu
def __getitem__(self, key):
"""Hỗ trợ cả indexing và slicing."""
if isinstance(key, slice):
# Trả về DataBuffer mới cho slices
return DataBuffer(self._du_lieu[key])
return self._du_lieu[key]
def __len__(self):
return len(self._du_lieu)
buffer = DataBuffer([1, 2, 3, 4, 5])
buffer[0] # 1
buffer[1:3] # DataBuffer([2, 3])
buffer[::2] # DataBuffer([1, 3, 5])__call__ - Object có thể Gọi được
Làm cho object hoạt động như một function.
python
class BoNhan:
def __init__(self, he_so: int):
self.he_so = he_so
def __call__(self, gia_tri: int) -> int:
"""obj(gia_tri) → gia_tri * he_so."""
return gia_tri * self.he_so
gap_doi = BoNhan(2)
gap_ba = BoNhan(3)
gap_doi(5) # 10 (gọi __call__)
gap_ba(5) # 15Trường hợp sử dụng: Hàm có thể Cấu hình
python
class ThuLai:
"""Decorator class với cấu hình."""
def __init__(self, so_lan_thu: int = 3):
self.so_lan_thu = so_lan_thu
def __call__(self, func):
def wrapper(*args, **kwargs):
for lan_thu in range(self.so_lan_thu):
try:
return func(*args, **kwargs)
except Exception as e:
if lan_thu == self.so_lan_thu - 1:
raise
print(f"Lần thử {lan_thu + 1} thất bại: {e}")
return wrapper
@ThuLai(so_lan_thu=5)
def goi_api_khong_on_dinh():
...Context Manager: __enter__ & __exit__
Implement giao thức with.
python
class BoDoThoiGian:
def __enter__(self):
import time
self.bat_dau = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.da_troi_qua = time.perf_counter() - self.bat_dau
print(f"Đã trôi qua: {self.da_troi_qua:.4f}s")
return False # Không nuốt exceptions
# Sử dụng
with BoDoThoiGian() as t:
# Code cần đo thời gian
sum(range(1000000))
# Output: Đã trôi qua: 0.0234sMagic Methods Toán học
python
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other: "Vector") -> "Vector":
"""v1 + v2"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other: "Vector") -> "Vector":
"""v1 - v2"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, he_so: float) -> "Vector":
"""v * 2"""
return Vector(self.x * he_so, self.y * he_so)
def __rmul__(self, he_so: float) -> "Vector":
"""2 * v (phép nhân phản chiếu)"""
return self.__mul__(he_so)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2 # Vector(4, 6)
v1 - v2 # Vector(-2, -2)
v1 * 3 # Vector(3, 6)
3 * v1 # Vector(3, 6) - dùng __rmul____slots__ - Tối ưu Memory
Mặc định, Python lưu attributes trong __dict__ — một dictionary linh hoạt nhưng tốn bộ nhớ. __slots__ thay thế bằng fixed array.
Memory Benchmark
python
import sys
class UserNoSlots:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
class UserWithSlots:
__slots__ = ('name', 'age')
def __init__(self, name: str, age: int):
self.name = name
self.age = age
# So sánh memory
no_slots = UserNoSlots("HPN", 30)
with_slots = UserWithSlots("HPN", 30)
# Kích thước object
sys.getsizeof(no_slots) # ~48 bytes (object header)
sys.getsizeof(no_slots.__dict__) # ~104 bytes (dict overhead)
# Tổng: ~152 bytes
sys.getsizeof(with_slots) # ~56 bytes (tất cả!)
# Tiết kiệm: ~63% memory per object!Khi nào dùng __slots__
python
# ✅ TỐT: Nhiều instances, ít attributes
class Point:
__slots__ = ('x', 'y')
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# Tạo 1 triệu points
points = [Point(i, i) for i in range(1_000_000)]
# Tiết kiệm ~100MB RAM so với không dùng slots!
# ❌ KHÔNG NÊN: Class cần dynamic attributes
class Config:
__slots__ = ('host', 'port')
config = Config()
config.host = "localhost"
config.timeout = 30 # AttributeError! Không có trong slots__slots__ với Inheritance
python
class Base:
__slots__ = ('x',)
class Derived(Base):
__slots__ = ('y',) # Chỉ thêm slots MỚI
def __init__(self, x: int, y: int):
self.x = x # Từ Base
self.y = y # Từ Derived
# ⚠️ CẢNH BÁO: Nếu parent không có slots, child có slots vẫn có __dict__
class NoSlots:
pass
class HasSlots(NoSlots):
__slots__ = ('value',)
obj = HasSlots()
obj.value = 1
obj.dynamic = 2 # Vẫn hoạt động! (từ __dict__ của parent)⚠️ SLOTS GOTCHAS
- Không thể thêm attributes động
- Không thể dùng
__dict__(trừ khi thêm vào slots) - Multiple inheritance phức tạp hơn
- Không hoạt động với
@dataclass(mặc định)
__init_subclass__ - Hook khi Class được Kế thừa
Python 3.6+ cho phép parent class "biết" khi có subclass mới. Rất hữu ích cho plugin systems và validation.
Cơ bản
python
class Plugin:
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str | None = None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__
Plugin._registry[name] = cls
print(f"Registered plugin: {name}")
class AudioPlugin(Plugin, plugin_name="audio"):
pass
class VideoPlugin(Plugin): # Dùng tên class làm mặc định
pass
# Output:
# Registered plugin: audio
# Registered plugin: VideoPlugin
print(Plugin._registry)
# {'audio': <class 'AudioPlugin'>, 'VideoPlugin': <class 'VideoPlugin'>}Validation Pattern
python
class Serializable:
"""Base class yêu cầu subclass phải implement serialize()."""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Kiểm tra method bắt buộc
if not hasattr(cls, 'serialize') or not callable(getattr(cls, 'serialize')):
raise TypeError(
f"{cls.__name__} must implement serialize() method"
)
class User(Serializable):
def serialize(self) -> dict:
return {"type": "user"}
class BadClass(Serializable): # TypeError ngay khi define class!
passSo sánh với Metaclass
python
# __init_subclass__ - Đơn giản, đủ cho hầu hết use cases
class Base:
def __init_subclass__(cls, **kwargs):
cls.created_at = "2024"
# Metaclass - Mạnh hơn nhưng phức tạp hơn
class Meta(type):
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
cls.created_at = "2024"
return cls💡 KHI NÀO DÙNG __init_subclass__
- Plugin registration
- Validation khi define class
- Auto-configuration
- Thay thế metaclass đơn giản
__class_getitem__ - Generic Classes
Python 3.9+ cho phép tạo generic classes với syntax MyClass[Type].
Cơ bản
python
from typing import TypeVar, Generic
T = TypeVar('T')
class Box(Generic[T]):
"""Generic container - cách truyền thống."""
def __init__(self, value: T):
self.value = value
# Sử dụng
int_box: Box[int] = Box(42)
str_box: Box[str] = Box("hello")Custom __class_getitem__
python
class Response:
"""Custom generic class không cần Generic base."""
def __class_getitem__(cls, item):
"""Được gọi khi: Response[SomeType]"""
# Trả về GenericAlias hoặc custom type
return type(f'Response[{item.__name__}]', (cls,), {
'__origin__': cls,
'__args__': (item,),
'_item_type': item
})
def __init__(self, data, status: int = 200):
self.data = data
self.status = status
# Type hints hoạt động
def get_user() -> Response[dict]:
return Response({"name": "HPN"})
# Runtime introspection
UserResponse = Response[dict]
print(UserResponse._item_type) # <class 'dict'>Pattern: Typed Container
python
from typing import Any
class TypedList:
"""List với runtime type checking."""
_item_type: type = object
def __class_getitem__(cls, item_type: type):
"""TypedList[int] → TypedList với _item_type = int"""
class TypedListInstance(cls):
_item_type = item_type
TypedListInstance.__name__ = f'TypedList[{item_type.__name__}]'
return TypedListInstance
def __init__(self):
self._items: list = []
def append(self, item: Any) -> None:
if not isinstance(item, self._item_type):
raise TypeError(
f"Expected {self._item_type.__name__}, "
f"got {type(item).__name__}"
)
self._items.append(item)
def __repr__(self):
return f"{self.__class__.__name__}({self._items})"
# Sử dụng
int_list = TypedList[int]()
int_list.append(1)
int_list.append(2)
int_list.append("3") # TypeError: Expected int, got strPython 3.12+ Type Parameter Syntax
python
# Python 3.12+ - Syntax mới, gọn hơn
class Box[T]:
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.value
# Tương đương với:
# T = TypeVar('T')
# class Box(Generic[T]): ...
# Bounded type parameters
class NumberBox[T: (int, float)]:
def __init__(self, value: T):
self.value = value
# Constrained type parameters
class Comparable[T: Comparable]: # Self-referential
def compare(self, other: T) -> int: ...Production Pitfalls 🚨
Pitfall 1: Quên __hash__ khi override __eq__
python
# ❌ BUG: Object không thể dùng trong set/dict
class User:
def __init__(self, id: int):
self.id = id
def __eq__(self, other):
return self.id == other.id
# Quên __hash__!
users = {User(1), User(1)} # TypeError: unhashable type
# ✅ FIX: Implement cả hai
class User:
def __init__(self, id: int):
self.id = id
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.id == other.id
def __hash__(self):
return hash(self.id)Pitfall 2: __slots__ với Default Values
python
# ❌ BUG: Default value trong slots
class Config:
__slots__ = ('timeout',)
timeout = 30 # ValueError: 'timeout' in __slots__ conflicts
# ✅ FIX: Default trong __init__
class Config:
__slots__ = ('timeout',)
def __init__(self, timeout: int = 30):
self.timeout = timeoutPitfall 3: __repr__ gây Infinite Recursion
python
# ❌ BUG: Circular reference trong __repr__
class Node:
def __init__(self, value, parent=None):
self.value = value
self.parent = parent
def __repr__(self):
return f"Node({self.value}, parent={self.parent})" # Infinite loop!
# ✅ FIX: Chỉ show ID hoặc None
class Node:
def __repr__(self):
parent_info = f"id={id(self.parent)}" if self.parent else "None"
return f"Node({self.value}, parent={parent_info})"Pitfall 4: __init_subclass__ không gọi super()
python
# ❌ BUG: Phá vỡ MRO chain
class Base:
def __init_subclass__(cls, **kwargs):
# Quên super().__init_subclass__(**kwargs)
cls.registered = True
class Mixin:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.mixed = True
class Child(Base, Mixin): # Mixin.__init_subclass__ không được gọi!
pass
# ✅ FIX: Luôn gọi super()
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.registered = TruePitfall 5: __class_getitem__ không cache
python
# ❌ KHÔNG TỐI ƯU: Tạo class mới mỗi lần
class Container:
def __class_getitem__(cls, item):
return type(f'Container[{item}]', (cls,), {})
Container[int] is Container[int] # False! Khác object
# ✅ TỐI ƯU: Cache kết quả
class Container:
_cache: dict = {}
def __class_getitem__(cls, item):
if item not in cls._cache:
cls._cache[item] = type(f'Container[{item}]', (cls,), {})
return cls._cache[item]
Container[int] is Container[int] # True!Bảng Tóm tắt
python
# === BIỂU DIỄN CHUỖI ===
__str__(self) # str(obj), print(obj) - cho người dùng
__repr__(self) # repr(obj), debugging - cho dev
# === SO SÁNH ===
__eq__(self, other) # obj == other
__ne__(self, other) # obj != other
__lt__(self, other) # obj < other
__le__(self, other) # obj <= other
__gt__(self, other) # obj > other
__ge__(self, other) # obj >= other
__hash__(self) # hash(obj) - cho sets/dicts
# === CONTAINER ===
__len__(self) # len(obj)
__getitem__(self, key) # obj[key]
__setitem__(self, key, v) # obj[key] = v
__delitem__(self, key) # del obj[key]
__contains__(self, item) # item in obj
__iter__(self) # for x in obj
# === CÓ THỂ GỌI ===
__call__(self, ...) # obj(...)
# === CONTEXT MANAGER ===
__enter__(self) # with obj as x
__exit__(self, exc_type, exc_val, tb) # kết thúc with
# === TOÁN HỌC ===
__add__(self, other) # obj + other
__sub__(self, other) # obj - other
__mul__(self, other) # obj * other
__truediv__(self, other) # obj / other
__floordiv__(self, other) # obj // other
__mod__(self, other) # obj % other
__pow__(self, other) # obj ** other
# === MEMORY & CLASS CUSTOMIZATION ===
__slots__ # Tối ưu memory, fixed attributes
__init_subclass__(cls, **kw) # Hook khi class được kế thừa
__class_getitem__(cls, item) # MyClass[Type] syntax