- Hiểu decorator là "function nhận function, trả function". Giải mã syntax
@dec def f(): ...=f = dec(f). - Viết được decorator đơn giản (logging, timing) và decorator có argument (decorator factory).
- Hiểu vì sao cần
@functools.wraps— bảo toàn metadata của function gốc. - Hiểu thứ tự apply khi stack nhiều decorator: bottom-up.
- Dùng
@functools.cache/@lru_cacheđể memoize, đặc biệt cho recursion. - Hiểu context manager protocol:
__enter__và__exit__. Khi nào nên suppress exception. - Viết context manager bằng 2 cách: class với
__enter__/__exit__hoặc generator với@contextlib.contextmanager. - Quản lý nhiều resource: nested
with,ExitStackcho số resource động,suppressđể bỏ qua exception cụ thể.
1. Decorator — mental model
Decorator trong Python không phải "Decorator pattern" của Gang of Four. Tên trùng nhau nhưng khác concept. Decorator của Python là một callable (thường là function) nhận một callable và trả về một callable. Mục đích: bọc thêm hành vi (logging, timing, retry, caching, validation) mà không phải sửa code function gốc.
Syntax @decorator ở trên dòng def chỉ là syntax
sugar:
# Đây:
@decorator
def f():
pass
# Tương đương:
def f():
pass
f = decorator(f) # gán đè biến f bằng kết quả decorator(f)
Nói cách khác, sau khi def f evaluate xong, Python lập tức gọi decorator(f) và gán
kết quả vào tên f. Kết quả thường là một function khác (gọi là
wrapper) bao quanh function gốc.
Hình dung function gốc là cái quà. Decorator là người gói quà: nhận quà, bọc giấy, dán nơ, trả ra một gói quà đã đóng gói. Bạn (caller) cầm gói quà gọi như cầm chính món quà — bên trong nó tự mở ra, làm thêm gì đó (logging, timing), rồi mới đưa cho function thật.
Bạn vẫn dùng f(x) như cũ, nhưng f bây giờ là wrapper, không phải function gốc nữa.
Function gốc bị "ẩn" bên trong wrapper qua closure.
Quan trọng — decorator chạy tại thời điểm def được evaluate, không phải khi gọi f():
def noisy(fn):
print(f"decorator đang wrap {fn.__name__}")
return fn
@noisy
def hello():
print("hello")
# Đã in ra "decorator đang wrap hello" — NGAY KHI def evaluate
# Chưa hề gọi hello()
hello() # Bây giờ in "hello"
2. Viết decorator đơn giản
Decorator điển hình có 2 lớp: outer (nhận fn) và inner (wrapper thực sự được gọi). Inner phải
chấp nhận mọi argument bằng *args, **kwargs để wrap được mọi signature.
def trace(fn):
def wrapper(*args, **kwargs):
print(f"→ calling {fn.__name__}({args}, {kwargs})")
result = fn(*args, **kwargs)
print(f"← {fn.__name__} trả về {result!r}")
return result
return wrapper
@trace
def add(a, b):
return a + b
add(2, 3)
# → calling add((2, 3), {})
# ← add trả về 5
Phân tích từng dòng:
def trace(fn)— outer function, nhận function được wrap.def wrapper(*args, **kwargs)— inner function, sẽ thayadd. Signature linh hoạt để wrap mọi function.fn(*args, **kwargs)— gọi function gốc với đúng args caller truyền vào.return wrapper— outer trả về wrapper.@tracekhiếnadd = trace(add) = wrapper.
Vì wrapper capture biến fn qua closure (xem Chương 3), function gốc không bị mất.
Tại sao *args, **kwargs?
Nếu bạn viết def wrapper(a, b), decorator chỉ wrap được function có đúng signature (a, b).
Dùng *args, **kwargs để wrapper "trong suốt" — nhận mọi argument, chuyển hết xuống function gốc.
3. functools.wraps — bảo toàn metadata
Có một vấn đề tinh tế với decorator. Sau khi wrap, add không còn trỏ vào function gốc nữa, mà trỏ
vào wrapper. Hệ quả: metadata như __name__, __doc__, __module__,
__annotations__ của add bị thay bằng của wrapper:
def trace(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@trace
def add(a, b):
"""Cộng 2 số."""
return a + b
print(add.__name__) # 'wrapper' 🔥 (đáng lẽ 'add')
print(add.__doc__) # None 🔥 (mất docstring)
help(add) # Help on function wrapper... 🔥
Điều này phá vỡ help(), debugger, profiler, tài liệu tự sinh (Sphinx). Fix bằng
@functools.wraps(fn) trên wrapper — copy metadata từ
fn sang wrapper:
import functools
def trace(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@trace
def add(a, b):
"""Cộng 2 số."""
return a + b
print(add.__name__) # 'add' ✓
print(add.__doc__) # 'Cộng 2 số.' ✓
print(add.__wrapped__) # <function add> ← reference function gốc
functools.wraps copy: __module__, __name__, __qualname__,
__annotations__, __doc__, và set __wrapped__ trỏ về fn gốc.
Cái __wrapped__ hữu ích khi cần "unwrap" decorator để introspect function gốc (vd:
inspect.signature dùng nó).
Mọi decorator nên dùng @functools.wraps(fn) trên wrapper.
Không có ngoại lệ trừ khi bạn cố ý không muốn (rất hiếm).
4. Decorator có argument — decorator factory
Đôi khi bạn cần decorator nhận tham số: @repeat(3),
@retry(times=3), @route('/users').
Cần thêm 1 lớp ngoài: decorator factory nhận argument, trả về decorator thật.
Phân tích chuyển hoá:
@repeat(3)
def greet():
print("hi")
# Tương đương:
def greet():
print("hi")
greet = repeat(3)(greet)
# ↑ gọi factory ↑ gọi decorator trả về
Vậy repeat(3) phải trả về một decorator (function nhận function trả function). Cấu trúc 3 tầng:
import functools
def repeat(n): # Tầng 1: factory, nhận args
def decorator(fn): # Tầng 2: decorator thật, nhận fn
@functools.wraps(fn)
def wrapper(*args, **kwargs): # Tầng 3: wrapper, replace fn
result = None
for _ in range(n):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"hi {name}")
greet("An")
# hi An
# hi An
# hi An
@repeat (không ngoặc) và @repeat(3) (có ngoặc)
là 2 chuyện khác nhau:
@repeat # f = repeat(f) — repeat nhận function!
@repeat() # f = repeat()(f) — repeat nhận () rồi mới wrap
@repeat(3) # f = repeat(3)(f)
Nếu bạn viết decorator factory mà không truyền arg, sẽ ra lỗi rất khó hiểu:
TypeError: 'function' object is not callable hoặc tương tự — vì repeat nhận f
tưởng đó là số n.
5. Stack decorator — thứ tự apply
Bạn có thể chồng nhiều decorator lên 1 function. Thứ tự apply là bottom-up (từ dưới lên):
decorator gần def nhất apply trước.
@a
@b
@c
def f():
pass
# Tương đương:
def f():
pass
f = a(b(c(f)))
# ↑ c apply trước (gần def nhất)
# ↑ rồi b
# ↑ rồi a (ngoài cùng)
Khi gọi f(x), control flow là top-down: a chạy trước, gọi b,
rồi b gọi c, rồi c gọi function gốc. Trả về thì reverse.
def make_dec(label):
def dec(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
print(f"{label} trước")
result = fn(*args, **kw)
print(f"{label} sau")
return result
return wrapper
return dec
@make_dec("A")
@make_dec("B")
@make_dec("C")
def f():
print(" f")
f()
# A trước
# B trước
# C trước
# f
# C sau
# B sau
# A sau
Order matters! Vd: @cache ngoài @trace
vs @trace ngoài @cache — kết quả khác:
@cachetrên cùng → cache hit không in trace (vì cache trả ngay, không gọi xuống).@tracetrên cùng → mỗi call đều log, kể cả cache hit.
6. functools.cache / lru_cache — memoization
Memoization là tối ưu kinh điển: lưu kết quả function theo argument, lần sau cùng args trả ngay từ cache. Python có sẵn 2 decorator:
@functools.cache— Python 3.9+. Cache không giới hạn.@functools.lru_cache(maxsize=128)— Có giới hạn (LRU eviction). Default 128.
import functools
import time
# Không cache: exponential O(2^n)
def fib_slow(n):
if n < 2:
return n
return fib_slow(n - 1) + fib_slow(n - 2)
t = time.perf_counter()
fib_slow(35) # ~2-3 giây
print(time.perf_counter() - t)
# Có cache: linear O(n) (sau khi memoize)
@functools.cache
def fib_fast(n):
if n < 2:
return n
return fib_fast(n - 1) + fib_fast(n - 2)
t = time.perf_counter()
fib_fast(500) # vài microseconds
print(time.perf_counter() - t)
print(fib_fast.cache_info()) # CacheInfo(hits=..., misses=..., ...)
Hai biến thể có signature cache giống nhau, chỉ khác policy eviction:
| — | @cache | @lru_cache(maxsize=N) |
|---|---|---|
| Python version | 3.9+ | 3.2+ |
| Giới hạn | Không giới hạn | Có, LRU eviction |
| Phù hợp khi | Pure function, args ít | Args nhiều, sợ leak memory |
| API | f.cache_clear(), f.cache_info() | Giống cache |
cache/lru_cache lưu kết quả vào dict, key là (args, kwargs) tuple. Argument phải
hashable — int, str, tuple, frozenset OK. list, dict, set sẽ
throw TypeError: unhashable type.
Workaround: convert sang tuple trước khi gọi, hoặc dùng frozenset.
7. Class-based decorator
Decorator không nhất thiết là function. Bất cứ object nào callable đều dùng được. Một class
với __call__ là decorator hợp lệ. Hữu ích khi cần giữ state giữa các lần gọi.
import functools
class CountCalls:
def __init__(self, fn):
functools.update_wrapper(self, fn) # thay cho @wraps
self.fn = fn
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.fn.__name__}")
return self.fn(*args, **kwargs)
@CountCalls
def greet(name):
print(f"Hi {name}")
greet("An") # Call #1 to greet
greet("Bình") # Call #2 to greet
print(greet.count) # 2 — truy cập state qua attribute
Khi @CountCalls apply, Python gọi CountCalls(greet) → tạo instance,
gán vào greet. Mỗi greet(...) thực ra là instance(...) → trigger
__call__.
Class-based decorator có argument (decorator factory) khác chút:
class Retry:
def __init__(self, times):
self.times = times
def __call__(self, fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
for i in range(self.times):
try:
return fn(*args, **kw)
except Exception:
if i == self.times - 1: raise
return wrapper
@Retry(times=3)
def flaky():
...
8. Decorator phổ biến trong stdlib và lib
Decorator có mặt khắp Python ecosystem. Một số tiêu biểu bạn sẽ gặp hằng ngày:
| Decorator | Module | Dùng làm gì |
|---|---|---|
@dataclass | dataclasses | Tự sinh __init__, __repr__, __eq__ (Chương 5) |
@property | builtin | Method → property getter (Chương 5) |
@classmethod | builtin | Method nhận cls thay self |
@staticmethod | builtin | Method không nhận self/cls |
@functools.cache | functools | Memoization (mục 6) |
@app.route('/users') | Flask | Đăng ký URL handler |
@app.get('/users') | FastAPI | Đăng ký GET endpoint |
@pytest.fixture | pytest | Khai báo fixture cho test (Chương 10) |
@pytest.mark.parametrize(...) | pytest | Chạy test với nhiều input |
@contextmanager | contextlib | Generator → context manager (mục 11) |
@asynccontextmanager | contextlib | Async context manager (Chương 8) |
Khi đọc framework code thấy @something, hỏi 3 câu: (1) nó wrap function thành cái
gì khác? (2) chạy lúc nào — at-import hay at-call? (3) thêm state/side-effect gì? Nắm 3 câu là hiểu được
decorator bất kỳ.
9. Context manager với with statement
Context manager giải quyết bài toán: "luôn cleanup resource khi xong, kể cả khi exception". Resource có thể là file, socket, lock, database connection, transaction, temp directory.
Cách viết "thủ công" với try/finally:
f = open("data.txt")
try:
content = f.read()
process(content) # nếu cái này throw...
finally:
f.close() # file vẫn được close. Tốt nhưng verbose.
Cách Pythonic với with:
with open("data.txt") as f:
content = f.read()
process(content)
# f.close() được gọi tự động khi ra khỏi with, kể cả khi exception
Cú pháp:
with EXPR as VAR:—EXPRlà biểu thức trả về context manager.VARbinding kết quả__enter__.- Phần
as VARlà optional. Có context manager không cần bind (vd lock). - Khi block kết thúc (bình thường hoặc do exception),
__exit__được gọi.
Các use case phổ biến:
import threading
# File — close tự động
with open("a.txt", "w") as f:
f.write("hi")
# Lock — release tự động
lock = threading.Lock()
with lock:
shared_state += 1
# Database connection (SQLite) — commit/rollback + close
import sqlite3
with sqlite3.connect("db.sqlite") as conn:
conn.execute("INSERT INTO ...")
# commit khi không exception, rollback nếu có
Nếu bạn quen C++, context manager chính là RAII (Resource Acquisition Is Initialization).
Khi vào block with: acquire resource. Khi ra block: release. Khác C++ destructor (gọi khi out of
scope), Python gọi __exit__ tại đúng điểm cuối with — deterministic, không phụ
thuộc GC.
Khác JS: JS không có cấu trúc tương đương. Bạn phải dùng try/finally hoặc disposable pattern
(TC39 explicit resource management đang propose). Đây là một ưu điểm của Python.
10. Viết context manager dạng class
Để một object dùng được trong with, nó phải implement context manager protocol — 2 dunder method:
__enter__(self)— chạy khi vào block. Return value được bind vàoas VAR.__exit__(self, exc_type, exc_val, exc_tb)— chạy khi ra block. Nhận info về exception (nếu có).
import time
class Timer:
def __init__(self, label="block"):
self.label = label
def __enter__(self):
self.start = time.perf_counter()
return self # bind vào `as t`
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self.start
print(f"{self.label}: {elapsed:.3f}s")
# không return gì = return None = không suppress exception
with Timer("loop") as t:
total = sum(range(10_000_000))
# loop: 0.142s
__exit__ arguments khi không có exception: cả 3 đều None.
__exit__ arguments khi có exception trong block: exc_type = class exception (vd ValueError), exc_val = instance exception, exc_tb = traceback object.
__exit__
Nếu __exit__ return True (truthy), Python nuốt exception — không
re-raise. Nếu return None/False (default), exception propagate ra ngoài.
Cẩn thận: chỉ suppress khi có lý do rõ (vd cleanup riêng cho lỗi này). Nuốt bừa exception là anti-pattern.
class IgnoreKeyError:
def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is KeyError:
print("Bỏ qua KeyError")
return True # suppress chỉ KeyError
# return None ngầm cho exception khác → re-raise
with IgnoreKeyError():
{}["missing"] # raise KeyError, nhưng bị nuốt
print("vẫn chạy tiếp")
11. @contextlib.contextmanager — generator thành context manager
Viết class với __enter__/__exit__ đôi khi dài dòng cho case đơn giản. contextlib
cung cấp một decorator biến generator function (yield đúng 1 lần) thành context manager. Cấu
trúc try / yield / finally:
from contextlib import contextmanager
import time
@contextmanager
def timer(label="block"):
start = time.perf_counter()
try:
yield start # value bind vào `as`
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
with timer("loop"):
total = sum(range(10_000_000))
# loop: 0.142s
Map ngữ nghĩa:
- Code trước
yield=__enter__(setup). - Giá trị
yield= giá trị trả về__enter__, bind vàoas. - Code trong
finally=__exit__(cleanup, luôn chạy). - Code trong
except(nếu có) = xử lý exception. Re-raise hoặc nuốt qua không re-raise.
Phiên bản đầy đủ với handle exception:
@contextmanager
def managed_resource(name):
print(f"acquire {name}")
res = acquire(name)
try:
yield res
except ValueError as e:
print(f"ValueError bị nuốt: {e}")
# không re-raise = suppress exception
finally:
print(f"release {name}")
release(res)
- Generator (
@contextmanager) — ngắn, đẹp cho case đơn giản, không có state phức tạp. - Class — khi có nhiều method/attribute, hoặc khi context manager là một thực thể có ý nghĩa riêng (vd
Connectionclass). - Cả 2 đều correct, chọn theo độ phức tạp.
12. Nested with — nhiều context manager trong 1 dòng
Khi cần nhiều resource cùng lúc, viết liền nhau cách bởi dấu phẩy. Tránh nested deep indent:
# Cách viết phổ biến (Python 3.1+)
with open("in.txt") as fin, open("out.txt", "w") as fout:
fout.write(fin.read().upper())
# Python 3.10+ cho phép parentheses để xuống dòng:
with (
open("in.txt") as fin,
open("out.txt", "w") as fout,
lock,
):
...
Thứ tự: cm bên trái được __enter__ trước, cm bên phải sau. Khi exit: ngược lại (phải trước, trái
sau) — đối xứng nested.
Câu hỏi quan trọng: with open(a) as f1, open(b) as f2: — nếu open(b) lỗi, f1
có được close không?
Có. Python đã __enter__ cm1 thành công, rồi __enter__ cm2 fail. Exception
bắt đầu propagate, Python trigger __exit__ của cm1 (đã enter rồi) → f1 close. Đây là behavior
đối xứng của with, không cần bạn làm gì.
13. contextlib.ExitStack — số resource động
Khi số lượng resource chỉ biết tại runtime (vd: mở N file theo input), không thể viết tĩnh
with open(a), open(b), .... Dùng ExitStack — collection-style context manager.
from contextlib import ExitStack
paths = ["a.txt", "b.txt", "c.txt"] # động, chỉ biết runtime
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
# tất cả file mở, cleanup khi ra block
for f in files:
process(f)
# Tất cả file được close, kể cả nếu giữa chừng có lỗi
API chính của ExitStack:
enter_context(cm)— push 1 context manager vào stack, gọi__enter__, trả value.callback(fn, *args, **kwargs)— đăng ký 1 function gọi khi cleanup (giốngdefercủa Go).push(cm)— push một cm đã enter từ trước.pop_all()— chuyển ownership cleanup sang ExitStack mới (advanced).
Ví dụ callback như defer:
with ExitStack() as stack:
conn = db.connect()
stack.callback(conn.close) # defer close
tmp = create_temp_file()
stack.callback(os.remove, tmp) # defer remove
do_work(conn, tmp)
# Cleanup chạy theo LIFO: remove tmp trước, rồi close conn
14. contextlib.suppress — bỏ qua exception cụ thể
Idiom "thử làm gì đó, nếu lỗi loại X thì bỏ qua, lỗi khác thì re-raise" rất phổ biến. Cách "thủ công":
import os
try:
os.remove("temp.txt")
except FileNotFoundError:
pass
Cách Pythonic ngắn hơn với suppress:
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("temp.txt")
# Nhiều exception type cùng lúc:
with suppress(FileNotFoundError, PermissionError):
os.remove("sensitive.log")
suppress chỉ "nuốt" đúng exception thuộc tuple đã chỉ định. Exception khác vẫn propagate bình
thường — không phải catch-all except Exception (đó là anti-pattern).
Vài utility khác trong contextlib hay dùng:
| Tool | Dùng làm gì |
|---|---|
contextmanager | Generator → cm (mục 11) |
ExitStack | Số cm động (mục 13) |
suppress | Bỏ qua exception cụ thể (mục 14) |
closing(obj) | Wrap object có .close() nhưng không phải cm, biến thành cm |
redirect_stdout(file) | Tạm chuyển sys.stdout sang file/io.StringIO |
redirect_stderr(file) | Tương tự cho stderr |
nullcontext(value) | cm "rỗng" — return value, không cleanup gì. Hữu ích cho code conditional |
asynccontextmanager | Async version (Chương 8) |
Triết lý của suppress chính là dòng "unless explicitly silenced" trong Zen of Python:
không catch bừa, nhưng cho phép tắt rõ ràng khi cần.
Bài tập
Bài 1 — @measure_time
Viết decorator measure_time log thời gian function chạy (giây, 4 chữ số thập phân).
- Phải hoạt động với mọi signature (
*args, **kwargs). - Phải bảo toàn metadata bằng
@functools.wraps. - In ra dạng
"<name>: 0.1234s".
Đáp án
import functools, time
def measure_time(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{fn.__name__}: {elapsed:.4f}s")
return wrapper
@measure_time
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum: 0.1421s
Lưu ý: dùng try/finally để log thời gian kể cả khi function throw — vẫn báo cáo thời gian đã chạy.
Bài 2 — Decorator factory @retry
Viết @retry(times=3, exceptions=(IOError,)) retry function khi gặp exception thuộc tuple exceptions.
- Default
times=3,exceptions=(Exception,). - Sleep
0.1 * attemptgiây giữa các lần retry (exponential-ish). - Sau khi hết retry vẫn lỗi → re-raise exception cuối.
- Exception không thuộc tuple → re-raise ngay, không retry.
Đáp án
import functools, time
def retry(times=3, exceptions=(Exception,)):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kw):
last_exc = None
for attempt in range(1, times + 1):
try:
return fn(*args, **kw)
except exceptions as e:
last_exc = e
print(f"attempt {attempt} failed: {e}")
if attempt < times:
time.sleep(0.1 * attempt)
raise last_exc
return wrapper
return decorator
@retry(times=3, exceptions=(IOError,))
def flaky():
import random
if random.random() < 0.8: raise IOError("network")
return "ok"
Exception ngoài tuple (vd ValueError) sẽ không bị bắt bởi except exceptions → propagate ngay, không retry.
Bài 3 — Context manager temp_file()
Viết context manager temp_file(suffix=".txt"):
- Tạo file tạm với suffix cho trước (gợi ý:
tempfile.mkstemp). yieldđường dẫn file ra cho block.- Khi exit (dù bình thường hay exception) → xoá file, kể cả khi đã không còn tồn tại (dùng
suppress).
Yêu cầu: viết bằng @contextmanager (generator).
Đáp án
import os, tempfile
from contextlib import contextmanager, suppress
@contextmanager
def temp_file(suffix=".txt"):
fd, path = tempfile.mkstemp(suffix=suffix)
os.close(fd) # mkstemp mở sẵn, close để user tự open lại
try:
yield path
finally:
with suppress(FileNotFoundError):
os.remove(path)
with temp_file() as p:
with open(p, "w") as f:
f.write("hello")
print(open(p).read())
# File đã bị xoá sau block
assert not os.path.exists(p)
Phiên bản class cũng OK (__enter__ tạo file, __exit__ xoá). Generator gọn hơn cho case này.
Bài 4 — @validate_types
Viết decorator validate_types đọc type hints của function, raise TypeError nếu
argument truyền vào không match hint.
- Gợi ý:
typing.get_type_hints(fn)trả dict{param_name: type}. - Gợi ý:
inspect.signature(fn).bind(*args, **kwargs)để map args đến param name. - Check bằng
isinstance(value, expected_type). - Bỏ qua key
'return'trong type hints.
Đáp án
import functools, inspect, typing
def validate_types(fn):
hints = typing.get_type_hints(fn)
sig = inspect.signature(fn)
@functools.wraps(fn)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
expected = hints.get(name)
if expected is None: continue
if not isinstance(value, expected):
raise TypeError(
f"{name}={value!r} không match {expected}"
)
return fn(*args, **kwargs)
return wrapper
@validate_types
def greet(name: str, age: int) -> str:
return f"{name} {age}"
greet("An", 25) # OK
greet("An", "25") # TypeError: age='25' không match <class 'int'>
Lưu ý: phiên bản đơn giản này không xử lý generic (vd list[int], Optional[X]).
Lib typeguard hoặc pydantic xử lý đầy đủ. Chương 9 (Type hints) sẽ đào sâu.
Bài 5 — Tìm bug: decorator mất docstring
Code dưới có bug — help(divide) không in ra docstring "Chia a cho b". Tìm và fix.
def safe(fn):
def wrapper(*args, **kw):
try:
return fn(*args, **kw)
except ZeroDivisionError:
return None
return wrapper
@safe
def divide(a, b):
"""Chia a cho b. Trả None nếu chia 0."""
return a / b
help(divide) # Help on function wrapper... ← BUG
print(divide.__name__) # 'wrapper' ← BUG
Bug + Fix
Bug: wrapper không copy metadata của divide. Sau khi
@safe apply, divide trỏ vào wrapper — nên
__name__ = 'wrapper', __doc__ = None.
Fix: dùng @functools.wraps(fn) trên wrapper.
import functools
def safe(fn):
@functools.wraps(fn) # ← thêm dòng này
def wrapper(*args, **kw):
try:
return fn(*args, **kw)
except ZeroDivisionError:
return None
return wrapper
# Bây giờ:
print(divide.__name__) # 'divide'
print(divide.__doc__) # 'Chia a cho b. Trả None nếu chia 0.'
Quiz
@dec def f(): ... — decorator dec chạy khi nào?
Xem đáp án
Tại thời điểm def f được evaluate (thường là lúc import module hoặc class
được parse), không phải khi gọi f(). Decorator gọi dec(f) ngay, kết quả gán vào
tên f. Đó là lý do mọi side-effect ngay trong outer của decorator chạy at-import-time.
Decorator stack @a @b def f(): ... apply theo thứ tự nào?
Xem đáp án
Bottom-up: b apply trước (gần def nhất), a apply sau. Cuối cùng:
f = a(b(f)). Khi gọi f() control flow ngược lại: a's wrapper chạy trước,
gọi b's wrapper, mới đến function gốc.
functools.wraps làm gì?
Xem đáp án
Copy metadata từ function gốc sang wrapper: __name__, __qualname__,
__module__, __doc__, __annotations__. Cũng set
wrapper.__wrapped__ = fn để introspection tool có thể "unwrap".
Không có nó, decorator phá help(), debugger và Sphinx docs.
__exit__ return True có ý nghĩa gì?
Xem đáp án
Suppress exception — Python không re-raise exception ra khỏi with block.
Default (return None hoặc False) thì exception propagate bình thường.
Suppress chỉ nên dùng khi có lý do rõ ràng (vd: cleanup chỉ áp dụng cho lỗi cụ thể này). Nuốt bừa exception là anti-pattern — debug khó kinh khủng.
with open(a) as f1, open(b) as f2: — nếu open(b) lỗi, f1 có được close?
Xem đáp án
Có. Python đã __enter__ cm1 thành công (mở a). Khi
open(b) throw exception, exception bắt đầu propagate. Vì cm1 đã enter, Python lập tức trigger
__exit__ của cm1 với info exception — f1.close() được gọi.
Đây là behavior đối xứng của multiple with — đảm bảo cleanup đúng cả khi acquire sau fail.
@functools.cache và @functools.lru_cache khác nhau thế nào?
Xem đáp án
cache (Python 3.9+) — cache không giới hạn, đơn giản hơn, có thể leak memory nếu
function chạy nhiều args khác nhau.
lru_cache(maxsize=128) — có giới hạn (LRU eviction), default 128, an toàn hơn cho long-running
process.
Cả 2 yêu cầu arguments hashable (int/str/tuple/frozenset OK; list/dict/set không OK). Cả
2 có fn.cache_info() và fn.cache_clear().
Class có __call__ có thể dùng làm decorator không?
Xem đáp án
Có. Decorator chỉ cần là callable (có thể gọi như function). Class với __call__
là callable hợp lệ. @MyClass tương đương f = MyClass(f) — tạo instance,
gán đè f. Mỗi f(...) sau đó trigger __call__.
Hữu ích khi cần giữ state (counter, registry) giữa các call mà không dùng global.
with và try/finally khác nhau gì? Thực ra with có làm gì "magic" không?
Xem đáp án
Về semantics không khác nhiều — with behind the scenes đúng là một
try/finally gọi __enter__/__exit__. Khác chủ yếu về syntax và
encapsulation:
withngắn hơn, ít boilerplate.- Logic acquire/release đóng gói trong context manager — caller không cần biết chi tiết.
- Dễ stack nhiều resource trên 1 dòng.
- Reusable — viết cm 1 lần, dùng nhiều nơi. Khó quên cleanup hơn.
Bonus: __exit__ nhận thông tin exception (exc_type, exc_val) → có thể
phân biệt "exit bình thường" vs "exit do lỗi". try/finally thuần phải dùng biến cờ riêng.
Tổng kết
Sau Chương 7 bạn nên đã master:
- Decorator = function nhận function trả function.
@dec def f=f = dec(f). Chạy at-import-time. - Mọi decorator nên dùng
@functools.wraps(fn)trên wrapper — bảo toàn metadata. - Decorator có argument = factory: 3 tầng (factory → decorator → wrapper).
- Stack decorator: bottom-up apply, top-down execute.
@a @b def f=a(b(f)). - Memoization với
@cache/@lru_cache— đặc biệt cho recursion. Args phải hashable. - Class-based decorator: class +
__call__. Tiện cho stateful decorator. - Decorator stdlib/lib:
@dataclass,@property,@route,@fixture… đâu cũng thấy. - Context manager với
with— RAII của Python, đảm bảo cleanup cả khi exception. - 2 cách viết cm: class với
__enter__/__exit__, hoặc generator với@contextmanager. __exit__returnTrue= suppress exception. Default = re-raise.- Nhiều cm: viết liền dấu phẩy, hoặc
ExitStackcho số động. suppress(ExceptionType)= idiom Pythonic cho "try / except / pass".
Kết nối
- Chương 3 (Functions & Closures) — wrapper trong decorator chính là closure capture
fn. Hiểu closure giúp hiểu decorator. - Chương 5 (OOP) —
@property,@classmethod,@staticmethod,@dataclassđều là decorator. Nay bạn hiểu chúng vận hành thế nào. - Chương 6 (Iterators & Generators) —
@contextmanagerbiến generator (yield 1 lần) thành cm. Đây chính là use case quan trọng của generator ngoài lazy iteration. - Chương 8 (Async Python) — Async có
@asynccontextmanagervàasync with. Concept giống hệt, chỉ là async version. - Chương 10 (Testing với pytest) —
@pytest.fixture,@pytest.mark.parametrizelà decorator. pytest fixture cũng dùng generator + yield giống@contextmanager. - JS Chương 7 (HOC/closures) — JS không có decorator syntax (TC39 stage-3 đang chờ), nhưng pattern HOC
withLogging(fn)tương đương Python decorator.