Skip to content

Magic Methods trong Python

Khi bạn viết len(my_list), Python gọi my_list.__len__(). Khi bạn viết a + b, Python gọi a.__add__(b). Magic methods — những phương thức có tên dạng __xxx__ — chính là protocol giúp cú pháp thanh lịch của Python hoạt động. Mọi toán tử, mọi built-in function, mọi câu lệnh with hay for đều được dịch về một lời gọi magic method tương ứng.

Trong production, magic methods xuất hiện ở khắp nơi: Django QuerySet override __getitem____iter__ để lazy-load dữ liệu từ database; SQLAlchemy Column dùng __eq__, __lt__ để xây dựng SQL WHERE clause thay vì so sánh giá trị Python; Pydantic model override __init____setattr__ để validate dữ liệu tự động khi gán. Hiểu magic methods nghĩa là hiểu cách các framework hàng đầu thiết kế API của họ.

Đừng để cái tên "magic" đánh lừa — đây không phải phép thuật. Đây là những protocol được định nghĩa rõ ràng trong Python Data Model. Khi bạn nắm vững chúng, bạn không chỉ sử dụng Python mà thực sự hiểu Python hoạt động như thế nào.


Bức tranh tư duy

Hãy hình dung magic methods như ổ cắm điện tiêu chuẩn. Ổ cắm có hai lỗ tròn (hoặc ba, tùy quốc gia) — bất kỳ thiết bị nào tuân theo đúng chuẩn đều cắm vào được. Bạn không cần biết bên trong ổ cắm dây đi thế nào; bạn chỉ cần biết giao diện (interface) là gì.

Tương tự, nếu class của bạn implement __len__, nó tự động hoạt động với len(), bool(), và bất kỳ hàm nào kỳ vọng một "sized object". Nếu implement __iter__, nó hoạt động với for loop, list(), unpacking *args, và comprehension. Bạn không cần đăng ký class ở đâu cả — chỉ cần cắm đúng "phích cắm" (implement đúng method), "ổ điện" Python sẽ nhận ra.

Cú pháp PythonMagic method được gọiGhi chú
str(obj) / print(obj)obj.__str__()Dành cho end-user
repr(obj)obj.__repr__()Dành cho developer
len(obj)obj.__len__()Phải trả về int >= 0
obj[key]obj.__getitem__(key)Subscription
obj == otherobj.__eq__(other)Equality
obj + otherobj.__add__(other)Addition
other + objobj.__radd__(other)Reflected addition
obj += otherobj.__iadd__(other)In-place addition
hash(obj)obj.__hash__()Dùng cho set/dict key
bool(obj)obj.__bool__()Fallback sang __len__
with obj as x:__enter__ / __exit__Context manager
obj.attrobj.__getattribute__('attr')Mọi attribute access
obj.missing_attrobj.__getattr__('missing_attr')Chỉ khi lookup thất bại

Khi analogy bị phá vỡ: Khác với ổ cắm vật lý (một thiết bị — một ổ), một class có thể implement nhiều protocol cùng lúc (vừa iterable, vừa sized, vừa comparable). Đây chính là sức mạnh của duck typing — "If it walks like a duck and quacks like a duck, it IS a duck."


Cốt lõi kỹ thuật

__str__ vs __repr__

Hai method này kiểm soát cách object được hiển thị dưới dạng text, nhưng phục vụ hai mục đích hoàn toàn khác nhau:

  • __str__: Dành cho end-user — ngắn gọn, dễ đọc, thân thiện.
  • __repr__: Dành cho developer — chi tiết, rõ ràng, giúp debug.

Golden Rule: __repr__ lý tưởng nhất nên trả về string mà khi paste vào REPL có thể tái tạo lại object (eval(repr(obj)) == obj). Không phải lúc nào cũng khả thi, nhưng đó là mục tiêu thiết kế.

python
from datetime import datetime
from decimal import Decimal


class Transaction:
    """Đại diện một giao dịch thanh toán trong hệ thống."""

    def __init__(self, tx_id: str, amount: Decimal, currency: str, timestamp: datetime):
        self.tx_id = tx_id
        self.amount = amount
        self.currency = currency
        self.timestamp = timestamp

    def __repr__(self) -> str:
        # Developer cần thấy MỌI field để debug
        return (
            f"Transaction(tx_id={self.tx_id!r}, amount={self.amount!r}, "
            f"currency={self.currency!r}, timestamp={self.timestamp!r})"
        )

    def __str__(self) -> str:
        # User chỉ cần thấy thông tin cốt lõi
        return f"[{self.tx_id}] {self.amount} {self.currency}"


# Sử dụng
tx = Transaction("TX-001", Decimal("150000.50"), "VND", datetime(2024, 6, 15, 10, 30))

print(str(tx))
# [TX-001] 150000.50 VND

print(repr(tx))
# Transaction(tx_id='TX-001', amount=Decimal('150000.50'),
#   currency='VND', timestamp=datetime.datetime(2024, 6, 15, 10, 30))

Quy tắc thực tế: Nếu chỉ implement một trong hai, hãy chọn __repr__. Khi __str__ không được định nghĩa, Python sẽ fallback sang __repr__. Ngược lại thì không.


__eq____hash__

Mặc định, Python so sánh object bằng identity (is) — hai object chỉ bằng nhau khi chúng cùng là một object trong bộ nhớ. Để so sánh bằng value, bạn phải tự implement __eq__.

QUY TẮC QUAN TRỌNG: Khi bạn định nghĩa __eq__, Python tự động set __hash__ = None, khiến object trở thành unhashable — không thể dùng làm key trong dict hay phần tử trong set. Lý do: nếu hai object bằng nhau (a == b), chúng bắt buộc phải có cùng hash (hash(a) == hash(b)). Python không muốn bạn vô tình vi phạm bất biến này.

python
import functools


@functools.total_ordering  # Tự động sinh __le__, __gt__, __ge__
class Version:
    """Semantic version với value equality."""

    def __init__(self, major: int, minor: int, patch: int):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return NotImplemented  # QUAN TRỌNG: không return False
        return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

    def __hash__(self) -> int:
        # Phải consistent với __eq__: equal objects → same hash
        return hash((self.major, self.minor, self.patch))

    def __repr__(self) -> str:
        return f"Version({self.major}, {self.minor}, {self.patch})"


# Value equality hoạt động
v1 = Version(3, 11, 0)
v2 = Version(3, 11, 0)
print(v1 == v2)      # True (so sánh value, không phải identity)
print(v1 is v2)      # False (hai object khác nhau trong bộ nhớ)

# Hashable → dùng được trong set/dict
versions = {v1, v2}
print(len(versions))  # 1 (vì v1 == v2 và hash bằng nhau)

# Ordering hoạt động nhờ @total_ordering
print(Version(3, 11, 0) < Version(3, 12, 0))  # True

NotImplemented vs NotImplementedError: Đây là hai thứ hoàn toàn khác nhau. return NotImplemented (không có Error) báo cho Python biết: "tôi không biết so sánh với kiểu này, hãy thử hỏi phía bên kia". raise NotImplementedError sẽ crash chương trình ngay lập tức.


__getattr__, __getattribute__, __setattr__

Ba method này kiểm soát toàn bộ cơ chế attribute access trong Python:

MethodKhi nào được gọiDùng khi nào
__getattribute__MỌI lần truy cập attributeRất hiếm khi cần override
__getattr__Chỉ khi attribute không tìm thấy qua cơ chế bình thườngProxy, delegation, lazy loading
__setattr__MỌI lần gán attributeValidation, logging, immutability
python
class APIProxy:
    """
    Proxy pattern: ủy quyền attribute access sang object khác.
    Ứng dụng: API client wrapper, lazy-loading, caching layer.
    """

    def __init__(self, target: object):
        # PHẢI dùng super().__setattr__ để tránh infinite recursion
        super().__setattr__('_target', target)
        super().__setattr__('_call_count', {})

    def __getattr__(self, name: str):
        """Chỉ gọi khi attribute không tìm thấy trên self."""
        # Đếm số lần truy cập mỗi attribute
        counts = super().__getattribute__('_call_count')
        counts[name] = counts.get(name, 0) + 1

        target = super().__getattribute__('_target')
        return getattr(target, name)

    def get_stats(self) -> dict:
        """Trả về thống kê truy cập."""
        return dict(super().__getattribute__('_call_count'))


# Sử dụng
class RealService:
    def fetch_user(self, user_id: int) -> str:
        return f"User-{user_id}"

    def fetch_order(self, order_id: int) -> str:
        return f"Order-{order_id}"


proxy = APIProxy(RealService())
print(proxy.fetch_user(42))    # "User-42" — delegated sang RealService
print(proxy.fetch_order(99))   # "Order-99"
print(proxy.fetch_user(10))    # "User-10"
print(proxy.get_stats())       # {'fetch_user': 2, 'fetch_order': 1}

Cảnh báo __getattribute__: Override method này cực kỳ nguy hiểm vì nó được gọi cho MỌI attribute access — kể cả khi bạn truy cập self.xxx bên trong chính __getattribute__. Nếu không cẩn thận, bạn sẽ tạo ra infinite recursion. Luôn dùng super().__getattribute__() khi cần truy cập attribute thực sự bên trong method này.


Operator overloading — __add__, __radd__, __iadd__

Python cho phép bạn định nghĩa hành vi cho mọi toán tử thông qua magic methods. Ba biến thể cần nắm:

  • __add__(self, other): Gọi khi self + other
  • __radd__(self, other): Gọi khi other + selfother.__add__ trả về NotImplemented
  • __iadd__(self, other): Gọi khi self += other (in-place, nên trả về self)
python
from __future__ import annotations


class Vector:
    """Vector 2D với đầy đủ operator overloading."""

    __slots__ = ('x', 'y')  # Tối ưu memory

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    # --- Representation ---
    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

    # --- Arithmetic ---
    def __add__(self, other: object) -> Vector:
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented

    def __radd__(self, other: object) -> Vector:
        # Cho phép sum() hoạt động (sum bắt đầu với 0 + first_element)
        if isinstance(other, int) and other == 0:
            return self
        return NotImplemented

    def __iadd__(self, other: Vector) -> Vector:
        if isinstance(other, Vector):
            self.x += other.x
            self.y += other.y
            return self  # PHẢI return self cho in-place operator
        return NotImplemented

    def __mul__(self, scalar: object) -> Vector:
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented

    def __rmul__(self, scalar: object) -> Vector:
        return self.__mul__(scalar)  # Phép nhân giao hoán

    # --- Comparison ---
    def __eq__(self, other: object) -> bool:
        if isinstance(other, Vector):
            return self.x == other.x and self.y == other.y
        return NotImplemented

    def __hash__(self) -> int:
        return hash((self.x, self.y))

    # --- Container protocol ---
    def __len__(self) -> int:
        return 2  # Vector 2D luôn có 2 thành phần

    def __abs__(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5


# Sử dụng
v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)          # Vector(4, 6)
print(v1 * 3)           # Vector(3, 6)
print(3 * v1)           # Vector(3, 6)  — nhờ __rmul__
print(abs(v1))           # 2.236...

# sum() hoạt động nhờ __radd__ xử lý 0 + Vector
vectors = [Vector(1, 0), Vector(0, 1), Vector(2, 3)]
print(sum(vectors))      # Vector(3, 4)

# In-place operator
v1 += v2
print(v1)               # Vector(4, 6)  — v1 đã thay đổi

Tại sao cần __radd__? Khi Python gặp a + b, nó thử a.__add__(b) trước. Nếu kết quả là NotImplemented, Python thử b.__radd__(a). Đây là cơ chế cho phép kiểu dữ liệu mới tương tác với kiểu dữ liệu có sẵn mà không cần sửa code kiểu cũ.


Context managers — __enter____exit__

Protocol with cho phép bạn quản lý tài nguyên (file, connection, lock) một cách an toàn — đảm bảo cleanup xảy ra ngay cả khi exception phát sinh.

python
import time
import sqlite3
from typing import Optional


class Timer:
    """Context manager đo thời gian thực thi."""

    def __init__(self, label: str = "Block"):
        self.label = label
        self.elapsed: Optional[float] = None

    def __enter__(self):
        self._start = time.perf_counter()
        return self  # Giá trị gán vào biến sau `as`

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self._start
        print(f"⏱ {self.label}: {self.elapsed:.4f}s")
        return False  # Không suppress exception


class DatabaseTransaction:
    """
    Context manager cho database transaction.
    Auto-commit nếu thành công, auto-rollback nếu exception.
    """

    def __init__(self, db_path: str):
        self.db_path = db_path
        self.conn: Optional[sqlite3.Connection] = None

    def __enter__(self) -> sqlite3.Connection:
        self.conn = sqlite3.connect(self.db_path)
        self.conn.execute("BEGIN")
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        if self.conn is None:
            return False

        if exc_type is not None:
            # Có exception → rollback
            self.conn.rollback()
            print(f"❌ Transaction rolled back: {exc_val}")
        else:
            # Không exception → commit
            self.conn.commit()
            print("✅ Transaction committed")

        self.conn.close()
        return False  # KHÔNG suppress exception — để caller xử lý


class FileLock:
    """
    Context manager cho file-based locking.
    Đảm bảo chỉ một process truy cập resource tại một thời điểm.
    """
    # <!-- NOTE: CẦN VERIFY — behavior trên Windows có thể khác Linux -->

    def __init__(self, lock_path: str):
        self.lock_path = lock_path
        self._fd = None

    def __enter__(self):
        import os
        self._fd = os.open(self.lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        import os
        if self._fd is not None:
            os.close(self._fd)
            os.unlink(self.lock_path)
        return False


# Sử dụng
with Timer("Data processing"):
    total = sum(range(1_000_000))
# ⏱ Data processing: 0.0312s

Ba tham số của __exit__:

Tham sốÝ nghĩaGiá trị khi không có exception
exc_typeClass của exceptionNone
exc_valInstance của exceptionNone
exc_tbTraceback objectNone

Return True trong __exit__ sẽ suppress (nuốt) exception — block with sẽ không raise gì cả. Chỉ dùng khi bạn biết chắc mình muốn bỏ qua lỗi đó. Trong 99% trường hợp, return False (hoặc không return gì — mặc định là None, tương đương False).


Thực chiến

Tình huống: Xây dựng class Money cho hệ thống thanh toán

Trong hệ thống e-commerce, xử lý tiền tệ là một trong những bài toán dễ sai nhất. Sử dụng float cho tiền → bug. Không kiểm tra currency khi cộng → bug. Đây là cách implement đúng với magic methods:

python
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from functools import total_ordering
from typing import Union


@total_ordering
class Money:
    """
    Immutable Money class cho hệ thống thanh toán.
    
    Quy tắc thiết kế:
    - Dùng Decimal thay vì float (tránh lỗi floating point)
    - Immutable → an toàn làm dict key
    - Cấm cộng/trừ khác currency (raise ValueError)
    - Hỗ trợ nhân với scalar (tính thuế, discount)
    """

    __slots__ = ('_amount', '_currency')

    def __init__(self, amount: Union[str, int, Decimal], currency: str = "VND"):
        # Dùng str → Decimal để tránh lỗi float
        object.__setattr__(self, '_amount', Decimal(str(amount)))
        object.__setattr__(self, '_currency', currency.upper())

    def __setattr__(self, name: str, value: object) -> None:
        raise AttributeError(
            f"Money is immutable. Cannot set '{name}'."
        )

    # --- Properties ---
    @property
    def amount(self) -> Decimal:
        return self._amount

    @property
    def currency(self) -> str:
        return self._currency

    # --- Representation ---
    def __repr__(self) -> str:
        return f"Money({self._amount!r}, {self._currency!r})"

    def __str__(self) -> str:
        if self._currency == "VND":
            formatted = f"{self._amount:,.0f}"
            return f"{formatted} ₫"
        return f"{self._amount:.2f} {self._currency}"

    # --- Equality & Hashing ---
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return self._amount == other._amount and self._currency == other._currency

    def __hash__(self) -> int:
        return hash((self._amount, self._currency))

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        self._check_currency(other)
        return self._amount < other._amount

    # --- Arithmetic ---
    def __add__(self, other: object) -> Money:
        if not isinstance(other, Money):
            return NotImplemented
        self._check_currency(other)
        return Money(self._amount + other._amount, self._currency)

    def __sub__(self, other: object) -> Money:
        if not isinstance(other, Money):
            return NotImplemented
        self._check_currency(other)
        return Money(self._amount - other._amount, self._currency)

    def __mul__(self, factor: object) -> Money:
        if isinstance(factor, (int, float, Decimal)):
            result = self._amount * Decimal(str(factor))
            return Money(result.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), self._currency)
        return NotImplemented

    def __rmul__(self, factor: object) -> Money:
        return self.__mul__(factor)

    def __neg__(self) -> Money:
        return Money(-self._amount, self._currency)

    # --- Boolean ---
    def __bool__(self) -> bool:
        return self._amount != 0

    # --- Helpers ---
    def _check_currency(self, other: Money) -> None:
        if self._currency != other._currency:
            raise ValueError(
                f"Cannot operate on {self._currency} and {other._currency}. "
                f"Convert currencies first."
            )

    def round_to(self, places: int = 0) -> Money:
        quantizer = Decimal(10) ** -places
        rounded = self._amount.quantize(quantizer, rounding=ROUND_HALF_UP)
        return Money(rounded, self._currency)


# === DEMO ===

# Tạo Money objects
price = Money("150000", "VND")
tax = Money("15000", "VND")
discount_rate = Decimal("0.1")

# Arithmetic
total = price + tax
print(total)              # 165,000 ₫

discounted = total * (1 - discount_rate)
print(discounted)         # 148,500 ₫

# Comparison
print(price > tax)        # True
print(price == Money("150000", "VND"))  # True (value equality)

# Hashable → dùng làm dict key
price_map = {
    Money("150000", "VND"): "Sản phẩm A",
    Money("250000", "VND"): "Sản phẩm B",
}
print(price_map[Money("150000", "VND")])  # "Sản phẩm A"

# Immutable → không thể thay đổi
try:
    price.amount = Decimal("999")
except AttributeError as e:
    print(f"Error: {e}")  # Money is immutable. Cannot set 'amount'.

# Currency safety
usd = Money("100", "USD")
try:
    price + usd
except ValueError as e:
    print(f"Error: {e}")  # Cannot operate on VND and USD...

# String formatting
print(repr(price))        # Money(Decimal('150000'), 'VND')
print(str(price))         # 150,000 ₫

usd_price = Money("99.99", "USD")
print(str(usd_price))    # 99.99 USD

Điểm đáng chú ý trong thiết kế:

  1. Immutable (qua __setattr__ raise error) → an toàn dùng trong set/dict và tránh side-effect
  2. Decimal thay vì float0.1 + 0.2 == 0.3 thực sự đúng
  3. Currency check → không bao giờ vô tình cộng VND với USD
  4. NotImplemented thay vì raise TypeError → cho Python cơ hội thử reflected method

Sai lầm điển hình

Sai lầm 1: Định nghĩa __eq__ mà quên __hash__

python
# SAI — object trở thành unhashable
class Product:
    def __init__(self, sku: str, name: str):
        self.sku = sku
        self.name = name

    def __eq__(self, other):
        if isinstance(other, Product):
            return self.sku == other.sku
        return NotImplemented

# Hậu quả:
p = Product("SKU-001", "Keyboard")
products = {p}  # TypeError: unhashable type: 'Product'
python
# ĐÚNG — luôn implement __hash__ cùng __eq__
class Product:
    def __init__(self, sku: str, name: str):
        self.sku = sku
        self.name = name

    def __eq__(self, other):
        if isinstance(other, Product):
            return self.sku == other.sku
        return NotImplemented

    def __hash__(self):
        return hash(self.sku)  # Consistent với __eq__

Impact: Trong production, bạn thường lưu object vào set để loại trùng, hoặc dùng object làm dict key. Nếu quên __hash__, code sẽ crash ở những nơi không ngờ tới — đặc biệt khi dữ liệu đi qua nhiều layer.


Sai lầm 2: __str__ trả về kiểu không phải string

python
# SAI — return int thay vì str
class Score:
    def __init__(self, value: int):
        self.value = value

    def __str__(self):
        return self.value  # TypeError: __str__ returned non-string

# ĐÚNG
class Score:
    def __init__(self, value: int):
        self.value = value

    def __str__(self):
        return str(self.value)  # Luôn return str

Impact: TypeError xảy ra khi print(), logging, hoặc f-string sử dụng object — thường ở production logging pipeline, khiến bạn mất cả log lẫn thông tin debug.


Sai lầm 3: Infinite recursion trong __getattr__

python
# SAI — truy cập self.data bên trong __getattr__ gây infinite loop
class Config:
    def __init__(self):
        self.data = {"debug": True}

    def __getattr__(self, name):
        # self.data ở đây lại gọi __getattr__ vì data chưa tồn tại
        # trong quá trình __init__ nếu có lỗi
        return self.data.get(name)  # RecursionError!
python
# ĐÚNG — dùng super().__getattribute__ hoặc __dict__
class Config:
    def __init__(self):
        self.data = {"debug": True}

    def __getattr__(self, name):
        # Truy cập trực tiếp __dict__ để bypass __getattr__
        data = self.__dict__.get('data', {})
        if name in data:
            return data[name]
        raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")

Impact: RecursionError crash toàn bộ request trong web server. Đặc biệt khó debug vì stack trace dài hàng trăm dòng lặp lại cùng một method.


Sai lầm 4: Return False thay vì NotImplemented trong comparison

python
# SAI — return False chặn Python thử reflected method
class Temperature:
    def __init__(self, celsius: float):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return False  # SAI! Nên return NotImplemented

# Hậu quả: mixed-type comparison luôn False,
# kể cả khi other có __eq__ biết so sánh với Temperature
python
# ĐÚNG
class Temperature:
    def __init__(self, celsius: float):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented  # Cho Python thử other.__eq__(self)
        return self.celsius == other.celsius

Impact: Khi bạn tích hợp với library bên thứ ba (ví dụ testing framework, ORM), return False sẽ âm thầm trả về kết quả sai mà không có error nào — bug kiểu "silent failure" khó phát hiện nhất.


Under the Hood

CPython dispatch toán tử như thế nào

Khi CPython gặp biểu thức a + b, nó không đơn giản gọi a.__add__(b). Quy trình thực tế phức tạp hơn:

  1. CPython tra cứu slot function nb_add trong PyTypeObject (file typeobject.c)
  2. Nếu type(b) là subclass của type(a), Python ưu tiên b.__radd__(a) trước (để subclass có thể override hành vi)
  3. Nếu không, thử a.__add__(b)
  4. Nếu kết quả là NotImplemented, thử b.__radd__(a)
  5. Nếu vẫn NotImplemented, raise TypeError
python
# Ví dụ minh họa subclass priority
class Base:
    def __add__(self, other):
        print("Base.__add__")
        return NotImplemented

class Sub(Base):
    def __radd__(self, other):
        print("Sub.__radd__")
        return "Sub wins"

result = Base() + Sub()
# In ra: Sub.__radd__ (KHÔNG phải Base.__add__)
# Vì Sub là subclass của Base → Sub.__radd__ được ưu tiên

Descriptor protocol đằng sau method binding

Khi bạn viết obj.method(), điều thực sự xảy ra:

  1. Python gọi type(obj).__dict__['method'].__get__(obj, type(obj))
  2. function.__get__ trả về một bound method — function đã được bind với self=obj
  3. Đó là lý do self tự động được truyền vào
python
class MyClass:
    def greet(self):
        return "Hello"

obj = MyClass()

# Hai cách gọi tương đương:
obj.greet()                    # Bound method call
MyClass.greet(obj)             # Unbound call, truyền self thủ công
MyClass.__dict__['greet'].__get__(obj, MyClass)()  # Descriptor protocol

__slots__ và ảnh hưởng tới attribute access

Khi class sử dụng __slots__, Python không tạo __dict__ cho mỗi instance. Thay vào đó, attribute được lưu trong một fixed-size array bên trong C struct:

python
import sys

class WithDict:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class WithSlots:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

d = WithDict(1, 2)
s = WithSlots(1, 2)

print(sys.getsizeof(d) + sys.getsizeof(d.__dict__))  # ~152 bytes (CPython 3.12)
print(sys.getsizeof(s))                                # ~56 bytes
# Tiết kiệm ~63% memory per instance

Trade-off: Class có __slots__ không thể thêm attribute động (obj.new_attr = value sẽ raise AttributeError), không hỗ trợ multiple inheritance phức tạp, và không thể dùng __dict__-dependent features.

Tại sao __hash__ bị set None khi định nghĩa __eq__

Đây không phải bug — đây là design decision có chủ đích, dựa trên nguyên tắc Liskov Substitution cho hashability:

Bất biến (invariant): Nếu a == b, thì BẮT BUỘC hash(a) == hash(b).

Nếu Python giữ nguyên __hash__ mặc định (dựa trên id()) khi bạn thay đổi __eq__ sang value equality, bất biến trên sẽ bị vi phạm. Hai object có cùng value nhưng khác id() sẽ có hash khác nhau → dictset hoạt động sai lầm lặng (silent data corruption).

Python chọn giải pháp an toàn: buộc bạn phải ý thức về vấn đề này bằng cách set __hash__ = None.


Checklist ghi nhớ

✅ Checklist triển khai

Representation (__str__ / __repr__)

  • [ ] __repr__ được implement cho MỌI class — cung cấp thông tin đủ để debug
  • [ ] __repr__ lý tưởng là eval()-able: eval(repr(obj)) == obj
  • [ ] __str__ chỉ implement khi cần hiển thị khác cho end-user
  • [ ] Cả hai luôn return str — không bao giờ return kiểu khác

Equality & Hashing

  • [ ] __eq__ return NotImplemented (không phải False) cho kiểu không hỗ trợ
  • [ ] Mỗi khi định nghĩa __eq__, đều xem xét implement __hash__
  • [ ] __hash__ consistent với __eq__: equal objects → same hash
  • [ ] Dùng @functools.total_ordering khi cần đầy đủ comparison operators

Operator Overloading

  • [ ] Implement __radd__ (và các reflected method) khi class cần tương tác với kiểu khác
  • [ ] __iadd__ (in-place) phải return self
  • [ ] Return NotImplemented thay vì raise TypeError cho kiểu không hỗ trợ
  • [ ] Kiểm tra isinstance trước khi xử lý — tránh crash khi nhận kiểu không mong đợi

Context Managers

  • [ ] __enter__ return giá trị hữu ích (thường là self hoặc resource)
  • [ ] __exit__ LUÔN thực hiện cleanup — kể cả khi có exception
  • [ ] __exit__ return False trừ khi có lý do rõ ràng để suppress exception
  • [ ] Cleanup code trong __exit__ không được raise exception mới (dùng try/except bảo vệ)

Bài tập luyện tập

Bài 1: Temperature class (Foundation)

Implement class Temperature với các yêu cầu:

  • Lưu trữ nhiệt độ dưới dạng Celsius (nội bộ)
  • __str__: hiển thị dạng "36.6°C"
  • __repr__: hiển thị dạng "Temperature(36.6)"
  • __eq__: so sánh hai Temperature bằng value
  • __add__: cộng hai Temperature (trả về Temperature mới)
  • __lt__: so sánh nhỏ hơn
  • Method to_fahrenheit(): chuyển đổi sang Fahrenheit
python
# Test cases bài tập phải pass:
t1 = Temperature(36.6)
t2 = Temperature(37.0)

assert str(t1) == "36.6°C"
assert repr(t1) == "Temperature(36.6)"
assert t1 < t2
assert t1 + t2 == Temperature(73.6)
assert t1 != t2
assert t1.to_fahrenheit() == 97.88  # 36.6 * 9/5 + 32
🔑 Lời giải Bài 1
python
from functools import total_ordering


@total_ordering
class Temperature:
    __slots__ = ('celsius',)

    def __init__(self, celsius: float):
        self.celsius = float(celsius)

    def __repr__(self) -> str:
        return f"Temperature({self.celsius})"

    def __str__(self) -> str:
        return f"{self.celsius}°C"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

    def __hash__(self) -> int:
        return hash(self.celsius)

    def __add__(self, other: object):
        if not isinstance(other, Temperature):
            return NotImplemented
        return Temperature(self.celsius + other.celsius)

    def to_fahrenheit(self) -> float:
        return round(self.celsius * 9 / 5 + 32, 2)

Bài 2: Database Transaction Context Manager (Intermediate)

Implement TransactionManager — context manager quản lý transaction cho một database connection giả lập:

  • __enter__: bắt đầu transaction, return connection
  • __exit__: commit nếu không có exception, rollback nếu có
  • Ghi log mọi hành động
  • Hỗ trợ nested transaction (savepoint)
python
# Interface mong đợi:
with TransactionManager(connection) as txn:
    txn.execute("INSERT INTO users (name) VALUES ('Alice')")
    txn.execute("UPDATE accounts SET balance = balance - 100")
    # Nếu exception → auto rollback
    # Nếu thành công → auto commit
🔑 Lời giải Bài 2
python
import logging

logger = logging.getLogger(__name__)


class FakeConnection:
    """Giả lập database connection cho bài tập."""

    def __init__(self):
        self.log = []
        self.in_transaction = False

    def execute(self, sql: str) -> None:
        self.log.append(f"EXECUTE: {sql}")

    def begin(self) -> None:
        self.in_transaction = True
        self.log.append("BEGIN")

    def commit(self) -> None:
        self.in_transaction = False
        self.log.append("COMMIT")

    def rollback(self) -> None:
        self.in_transaction = False
        self.log.append("ROLLBACK")

    def savepoint(self, name: str) -> None:
        self.log.append(f"SAVEPOINT {name}")

    def rollback_to(self, name: str) -> None:
        self.log.append(f"ROLLBACK TO {name}")

    def release_savepoint(self, name: str) -> None:
        self.log.append(f"RELEASE SAVEPOINT {name}")


class TransactionManager:
    """Context manager cho database transactions với nested support."""

    _depth = 0  # Theo dõi nesting level

    def __init__(self, connection: FakeConnection):
        self.conn = connection
        self._savepoint_name = None

    def __enter__(self) -> FakeConnection:
        TransactionManager._depth += 1
        if TransactionManager._depth == 1:
            self.conn.begin()
            logger.info("Transaction started")
        else:
            self._savepoint_name = f"sp_{TransactionManager._depth}"
            self.conn.savepoint(self._savepoint_name)
            logger.info(f"Savepoint created: {self._savepoint_name}")
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        try:
            if exc_type is not None:
                if self._savepoint_name:
                    self.conn.rollback_to(self._savepoint_name)
                    logger.warning(f"Rolled back to {self._savepoint_name}: {exc_val}")
                else:
                    self.conn.rollback()
                    logger.error(f"Transaction rolled back: {exc_val}")
            else:
                if self._savepoint_name:
                    self.conn.release_savepoint(self._savepoint_name)
                    logger.info(f"Savepoint released: {self._savepoint_name}")
                else:
                    self.conn.commit()
                    logger.info("Transaction committed")
        finally:
            TransactionManager._depth -= 1
        return False  # Không suppress exception


# Sử dụng
conn = FakeConnection()

with TransactionManager(conn) as txn:
    txn.execute("INSERT INTO users (name) VALUES ('Alice')")

    # Nested transaction (savepoint)
    try:
        with TransactionManager(conn) as txn_inner:
            txn_inner.execute("UPDATE accounts SET balance = 0")
            raise ValueError("Insufficient funds")
    except ValueError:
        pass  # Savepoint rolled back, outer transaction continues

    txn.execute("INSERT INTO logs (msg) VALUES ('completed')")

print(conn.log)
# ['BEGIN', 'EXECUTE: INSERT ...', 'SAVEPOINT sp_2',
#  'EXECUTE: UPDATE ...', 'ROLLBACK TO sp_2',
#  'EXECUTE: INSERT INTO logs ...', 'COMMIT']

🧠 Quiz

Kiểm tra kiến thức

Câu 1: Khi bạn viết a + ba.__add__(b) trả về NotImplemented, Python sẽ làm gì tiếp theo?

  • A) Raise TypeError ngay lập tức
  • B) Raise NotImplementedError
  • C) Gọi b.__radd__(a)Đáp án đúng
  • D) Trả về None

Giải thích: Python thử reflected method (__radd__) của operand bên phải trước khi raise TypeError. Đây là cơ chế cho phép kiểu dữ liệu mới tương tác với kiểu có sẵn.


Câu 2: Tại sao Python set __hash__ = None khi bạn định nghĩa __eq__?

  • A) Đây là bug của Python, sẽ được fix ở version sau
  • B) Vì hash mặc định (dựa trên id()) sẽ vi phạm bất biến a == b → hash(a) == hash(b)Đáp án đúng
  • C) Vì object có __eq__ không cần hash
  • D) Để tiết kiệm bộ nhớ

Giải thích: Hash mặc định dựa trên id() (địa chỉ bộ nhớ). Khi __eq__ so sánh theo value, hai object khác nhau trong bộ nhớ có thể equal nhưng có hash khác nhau → dict/set sẽ hoạt động sai.


Câu 3: __getattr____getattribute__ khác nhau thế nào?

  • A) Chúng giống nhau, chỉ là alias
  • B) __getattr__ gọi cho MỌI access, __getattribute__ chỉ khi thất bại
  • C) __getattribute__ gọi cho MỌI access, __getattr__ chỉ khi lookup thất bại ← Đáp án đúng
  • D) __getattr__ dùng cho class attribute, __getattribute__ dùng cho instance attribute

Giải thích: __getattribute__ là "gateway" cho mọi attribute access. __getattr__ chỉ là "fallback" khi cơ chế tìm kiếm bình thường (instance dict → class dict → base classes) không tìm thấy attribute.


Liên kết học tiếp