Skip to content

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ứcMục đíchĐối tượng
__str__Dễ đọcNgười dùng cuối
__repr__Rõ ràng, debugLậ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)    # 15

Trườ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.0234s

Magic 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 systemsvalidation.

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!
    pass

So 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 str

Python 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 = timeout

Pitfall 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 = True

Pitfall 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