Giao diện
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__ và __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__ và __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 Python | Magic method được gọi | Ghi 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 == other | obj.__eq__(other) | Equality |
obj + other | obj.__add__(other) | Addition |
other + obj | obj.__radd__(other) | Reflected addition |
obj += other | obj.__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.attr | obj.__getattribute__('attr') | Mọi attribute access |
obj.missing_attr | obj.__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__ và __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
NotImplementedvsNotImplementedError: Đâ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 NotImplementedErrorsẽ 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:
| Method | Khi nào được gọi | Dùng khi nào |
|---|---|---|
__getattribute__ | MỌI lần truy cập attribute | Rất hiếm khi cần override |
__getattr__ | Chỉ khi attribute không tìm thấy qua cơ chế bình thường | Proxy, delegation, lazy loading |
__setattr__ | MỌI lần gán attribute | Validation, 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ậpself.xxxbên trong chính__getattribute__. Nếu không cẩn thận, bạn sẽ tạo ra infinite recursion. Luôn dùngsuper().__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 khiself + other__radd__(self, other): Gọi khiother + selfvàother.__add__trả vềNotImplemented__iadd__(self, other): Gọi khiself += 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 đổiTại sao cần
__radd__? Khi Python gặpa + 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__ và __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.0312sBa tham số của __exit__:
| Tham số | Ý nghĩa | Giá trị khi không có exception |
|---|---|---|
exc_type | Class của exception | None |
exc_val | Instance của exception | None |
exc_tb | Traceback object | None |
Return True trong
__exit__sẽ suppress (nuốt) exception — blockwithsẽ 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, returnFalse(hoặc không return gì — mặc định làNone, tương đươngFalse).
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ế:
- Immutable (qua
__setattr__raise error) → an toàn dùng trongset/dictvà tránh side-effect - Decimal thay vì float →
0.1 + 0.2 == 0.3thực sự đúng - Currency check → không bao giờ vô tình cộng VND với USD
NotImplementedthay 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 strImpact: 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 Temperaturepython
# ĐÚ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.celsiusImpact: 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:
- CPython tra cứu slot function
nb_addtrongPyTypeObject(filetypeobject.c) - Nếu
type(b)là subclass củatype(a), Python ưu tiênb.__radd__(a)trước (để subclass có thể override hành vi) - Nếu không, thử
a.__add__(b) - Nếu kết quả là
NotImplemented, thửb.__radd__(a) - Nếu vẫn
NotImplemented, raiseTypeError
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ênDescriptor protocol đằng sau method binding
Khi bạn viết obj.method(), điều thực sự xảy ra:
- Python gọi
type(obj).__dict__['method'].__get__(obj, type(obj)) function.__get__trả về một bound method — function đã được bind vớiself=obj- Đó là lý do
selftự độ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 instanceTrade-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ỘChash(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 → dict và set 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__returnNotImplemented(không phảiFalse) 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_orderingkhi 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 returnself - [ ] Return
NotImplementedthay vì raiseTypeErrorcho kiểu không hỗ trợ - [ ] Kiểm tra
isinstancetrướ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àselfhoặc resource) - [ ]
__exit__LUÔN thực hiện cleanup — kể cả khi có exception - [ ]
__exit__returnFalsetrừ 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 + b và a.__add__(b) trả về NotImplemented, Python sẽ làm gì tiếp theo?
- A) Raise
TypeErrorngay 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ếna == 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__ và __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.