Bài 4 · Nâng cao · 22 phút· Cập nhật 11/06/2026
Decorator & closure
Biên soạn bởi Nguyễn Anh Tuấn
Closure và decorator trong Python: hàm hạng nhất (first-class), hàm bọc hàm, functools.wraps, lru_cache - tự viết decorator của riêng bạn.
Trong Python, hàm cũng là một giá trị: gán cho biến, bỏ vào list/dict, truyền vào hàm khác, và trả về từ một hàm. Đây là nền cho closure & decorator:
first_class.py
def chao(ten):
return f"Xin chao, {ten}"
f = chao # GAN ham (khong goi)
print(f("An"))
def ap_dung(ham, x): # nhan HAM lam doi so
return ham(x)
print(ap_dung(chao, "Binh")) Kết quả khi chạy
Xin chao, An Xin chao, Binh
- ▸chao là HÀM (giá trị); chao("An") là KẾT QUẢ gọi hàm - đừng nhầm.
- ▸Gán/truyền/trả về hàm được → mở ra closure & decorator.
- ▸lambda là cách viết hàm nhỏ không tên ngay tại chỗ.
Khi một hàm trong dùng biến của hàm bao quanh và được trả ra ngoài, nó “mang theo” các biến đó - gọi là closure. Bấm qua demo:
Các biến bị bắt sống thật trong các cell, xem được qua __closure__:
closure_cell.py - bản chất
def make():
n = 42
def inner():
return n # bat bien n cua ham bao ngoai
return inner
f = make()
print(f.__closure__[0].cell_contents) # 42 - n van song trong cell Kết quả khi chạy
42
- ▸make_counter tạo count rồi trả inc; inc vẫn nhớ count dù make_counter đã kết thúc.
- ▸Biến bị bắt nằm trong cell → ham.__closure__[i].cell_contents là giá trị thật.
- ▸Muốn SỬA biến bị bắt (count += 1) phải khai báo nonlocal count.
Bẫy late binding
Decorator nhận một hàm và trả về hàm mới (thường “bọc” thêm việc quanh hàm gốc). @log chỉ là đường tắt cho greet = log(greet) - xem lại demo “Decorator” trong widget trên, song song với:
decorator.py
def log(func):
def wrapper(*args, **kwargs):
print("[log] truoc khi goi")
kq = func(*args, **kwargs) # goi ham GOC
print("[log] sau khi goi")
return kq
return wrapper
@log # = greet = log(greet)
def greet(ten):
print(f"Xin chao, {ten}")
greet("An") Kết quả khi chạy
[log] truoc khi goi Xin chao, An [log] sau khi goi
- ▸@deco trên def f ≡ f = deco(f). Sau đó gọi f() thực ra gọi wrapper.
- ▸wrapper dùng *args, **kwargs để bọc được hàm có chữ ký bất kỳ.
- ▸Decorator thêm hành vi (log, đo giờ, kiểm quyền…) mà KHÔNG sửa hàm gốc.
Khi bọc, hàm kết quả mất tên/doc gốc. @functools.wraps chép lại metadata và đặt __wrapped__ trỏ về hàm gốc. Muốn decorator nhận tham số thì thêm một tầng hàm:
wraps_params.py
import functools
def repeat(n): # tang 1: nhan THAM SO
def deco(func): # tang 2: nhan HAM
@functools.wraps(func) # giu ten/doc; dat __wrapped__ = func
def wrapper(*args, **kwargs): # tang 3: nhan doi so loi goi
for _ in range(n):
kq = func(*args, **kwargs)
return kq
return wrapper
return deco
@repeat(3)
def hi():
print("hi")
hi()
print(hi.__name__) # 'hi' (nho wraps), khong phai 'wrapper'
print(hi.__wrapped__ is not None) # True: truy cap duoc ham goc Kết quả khi chạy
hi hi hi hi True
- ▸@functools.wraps(func) giữ __name__/__doc__ và đặt __wrapped__ = func - gần như luôn dùng.
- ▸__wrapped__ cho phép lấy lại hàm GỐC (gỡ lớp bọc) khi cần.
- ▸Decorator có tham số = 3 tầng: nhận tham số → nhận hàm → nhận đối số lời gọi.
Đặt nhiều decorator lên một hàm: @a trên @b nghĩa là f = a(b(f)). Trang trí từ dưới lên, chạy từ ngoài vào:
stacking.py
def a(f):
def wrap():
print("a: truoc"); f(); print("a: sau")
return wrap
def b(f):
def wrap():
print(" b: truoc"); f(); print(" b: sau")
return wrap
@a
@b
def hello(): # = hello = a(b(hello))
print(" hello")
hello() Kết quả khi chạy
a: truoc
b: truoc
hello
b: sau
a: sau- ▸@a @b def f ≡ f = a(b(f)): b áp TRƯỚC (gần hàm), a bọc NGOÀI.
- ▸Khi gọi: wrapper của a (ngoài cùng) chạy trước → b → hàm gốc → b ra → a ra.
- ▸Thứ tự quan trọng: vd @app.route phải ngoài cùng, @login_required bên trong.
Decorator phát huy tác dụng nhất ở những việc lặp lại ở nhiều nơi (cross-cutting concerns): cache, đo giờ, log, kiểm quyền. Thư viện chuẩn cho sẵn lru_cache - và còn báo cáo hiệu quả cache:
lru_cache.py
import functools
@functools.lru_cache # nho ket qua da tinh (memoization)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(20))
print(fib.cache_info()) # thong ke cache Kết quả khi chạy
6765 CacheInfo(hits=18, misses=21, maxsize=128, currsize=21)
- ▸@lru_cache biến hàm đệ quy chậm thành nhanh (chỉ với hàm thuần, đối số hashable).
- ▸cache_info() cho hits/misses/maxsize - đo hiệu quả cache thật.
- ▸Tự viết @timing (đo time.perf_counter) là bài tập decorator kinh điển.
Tổng kết & tiếp theo
Câu hỏi thường gặp
Nghĩa là hàm cũng chỉ là một GIÁ TRỊ như int hay str: gán cho biến, bỏ vào list, truyền vào hàm khác, và TRẢ VỀ từ một hàm. Chính vì vậy mới có closure và decorator. Lưu ý: greet là HÀM (giá trị); greet() là KẾT QUẢ gọi hàm.
Closure là một hàm “nhớ” được các biến của phạm vi bao quanh nơi nó được ĐỊNH NGHĨA - kể cả khi hàm ngoài đã kết thúc. Các biến đó được giữ trong các “cell”, truy cập qua ham.__closure__ (mỗi phần tử là một cell có .cell_contents). Đó là cơ chế thật khiến count “sống tiếp” giữa các lần gọi.
Khi hàm trong chỉ ĐỌC biến ngoài thì không cần gì. Nhưng khi muốn GÁN/sửa biến của hàm bao quanh (count += 1), Python mặc định coi đó là biến cục bộ mới → lỗi. Khai báo nonlocal count để nói “sửa biến count ở phạm vi ngoài”.
Là một HÀM nhận một hàm và trả về một hàm (thường là “wrapper”). Cú pháp @log đặt trên def greet chỉ là đường tắt cho greet = log(greet). Sau đó gọi greet() thực ra gọi wrapper - chạy thêm việc trước/sau hàm gốc mà không sửa hàm gốc.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Cú pháp @log đặt trên def greet là đường tắt cho cái gì?
- 1
Hàm là giá trị
Tạo dict ánh xạ tên phép tính → hàm (vd
{"cong": lambda a,b: a+b}). Gọi qua dict.Hoàn thành khi:
phep["cong"](2, 3)→ 5 - minh hoạ hàm là hạng nhất. - 2
Soi closure
Viết
make_countertrả vềinc; ininc.__closure__[0].cell_contentsđể thấy biến bị bắt.Hoàn thành khi: Bạn thấy giá trị
countnằm trong cell của closure; giải thích vì sao nó sống tiếp. - 3
Decorator + wraps
Viết
@logdùng@functools.wraps; kiểmham.__name__vẫn đúng vàham.__wrapped__trỏ tới hàm gốc.Hoàn thành khi:
__name__là tên gốc (không phải"wrapper");__wrapped__là hàm gốc. - 4
Xếp chồng
Đặt
@atrên@btrên một hàm, mỗi decorator in"vào"/"ra". Dự đoán thứ tự rồi chạy.Hoàn thành khi: Thứ tự:
avào →bvào → hàm →bra →ara. Bạn giải thíchf = a(b(f)). - 5
lru_cache
Trang trí
fibbằng@functools.lru_cache; gọifib(20); infib.cache_info().Hoàn thành khi: Có hits/misses (vd
hits=18, misses=21);fibnhanh hơn hẳn nhờ nhớ kết quả.