← Lập trình Python nâng cao

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:

1 def make_counter():
2 count = 0
3 def inc():
4 nonlocal count
5 count += 1
6 return count
7 return inc
8
9 c = make_counter()
10 print(c()); print(c()); print(c())
Biến
count = 0
Trả về / in
(chưa có)
bước 1/5
make_counter() chạy: tạo count = 0 trong phạm vi của nó.

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

Closure bắt BIẾN, không phải giá trị tại thời điểm tạo. [lambda: i for i in range(3)] cho ba hàm cùng trả 2 (giá trị i cuối). Khắc phục: chốt giá trị ngay - lambda i=i: i.

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 đặ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

Closure (hàm nhớ trạng thái, sống trong cell) và decorator (hàm bọc hàm, xếp chồng được) là hai công cụ “Pythonic” gặp khắp nơi (Flask/FastAPI route, pytest fixture, lru_cache…). Tiếp theo của khoá: type hints & mypy, rồi kiểm thử - và phần đồng thời.

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.

Không có wraps, hàm bọc mất tên & docstring gốc (greet.__name__ thành "wrapper"). @functools.wraps(func) SAO CHÉP metadata (tên, doc…) sang wrapper, và đặt thêm wrapper.__wrapped__ trỏ tới hàm GỐC - tiện để truy cập/gỡ lớp bọc. Gần như luôn nên dùng wraps khi viết decorator.

@a trên @b trên def f nghĩa là f = a(b(f)): b áp TRƯỚC (gần hàm nhất), a bọc NGOÀI. Khi gọi, wrapper của a chạy trước (ngoài cùng), rồi tới b, rồi hàm gốc, rồi quay ra. Quy tắc: trang trí từ DƯỚI LÊN, chạy từ NGOÀI vào.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Cú pháp @log đặt trên def greet là đường tắt cho cái gì?

  1. 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. 2

    Soi closure

    Viết make_counter trả về inc; in inc.__closure__[0].cell_contents để thấy biến bị bắt.

    Hoàn thành khi: Bạn thấy giá trị count nằm trong cell của closure; giải thích vì sao nó sống tiếp.

  3. 3

    Decorator + wraps

    Viết @log dùng @functools.wraps; kiểm ham.__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. 4

    Xếp chồng

    Đặt @a trên @b trê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ự: a vào → b vào → hàm → b ra → a ra. Bạn giải thích f = a(b(f)).

  5. 5

    lru_cache

    Trang trí fib bằng @functools.lru_cache; gọi fib(20); in fib.cache_info().

    Hoàn thành khi: Có hits/misses (vd hits=18, misses=21); fib nhanh hơn hẳn nhờ nhớ kết quả.