Học bằng cách đọc theo thứ tự

Decorator — Thêm hành vi cho hàm với @

Học decorator trong Python với cú pháp @ để thêm hành vi cho hàm mà không sửa code gốc, áp dụng được vào các framework thực tế.

Đến cuối bài trước về lambda, bạn đã thấy các cách chính để coi hàm như giá trị. Để khép lại, hãy chốt decorator — cú pháp chuyên dụng để xếp hành vi bổ sung lên trên một hàm.

Decorator là gì?

Decorator là cách thêm hành vi trước và sau một hàm mà không thay đổi chính hàm đó. Những thứ như "log lệnh gọi," "đo thời gian chạy," hay "cache kết quả" — hành vi chung mà bạn muốn xếp lên nhiều hàm — có thể sống ở một nơi.

Cú pháp chỉ là một dòng phía trên định nghĩa hàm: @tên_decorator. Python đọc đó là "giống func = tên_decorator(func)".

@ nghĩa là "Chạy hàm này qua hàm kia"
@loggerdef greet():greet = logger(greet)khai triển thành
Viết @logger phía trên def greet(): khiến Python chạy greet = logger(greet) bên trong, thay greet bằng một hàm mới được logger bao bọc.
# Chính decorator (một hàm bậc cao nhận và trả về một hàm)
def logger(func):
    def wrapper():
        print("=== bắt đầu ===")
        func()                       # gọi hàm gốc
        print("=== kết thúc ===")
    return wrapper

# Phía bên gọi: chỉ thêm @
@logger
def greet(): # → logger(greet)
    print("Xin chào")

greet()
# === bắt đầu ===
# Xin chào
# === kết thúc ===

# Tương đương bên trong với:
# def greet():
#     print("Xin chào")
# greet = logger(greet)

Decorator cơ bản — Bọc hàm bằng wrapper

Bộ khung của decorator là hình dạng 3 bước: hàm bên ngoài nhận func, hàm bên trong (theo quy ước là wrapper) gọi func(), và bạn return wrapper. wrapper giữ func khả dụng khi nó chạy chính xác là một closure.

Bất cứ thứ gì bạn viết trước và sau func() chạy mỗi lần hàm được trang trí được gọi.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] đang chạy {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} xong")
        return result
    return wrapper

@logger
def greet(name):
    return f"Xin chào, {name}"

print(greet("Minh"))
# [LOG] đang chạy greet
# [LOG] greet xong
# Xin chào, Minh
Cách decorator logger bọc một hàm
Module (Không gian tên Global)
  • greet được thay bằng hàm wrapper
  • Thân greet gốc sống tiếp dưới dạng func bên trong wrapper
Frame của logger(func)
  • func giữ greet gốc
  • Xây wrapper bên trong và trả về
wrapper (closure nhớ func)
  • Chạy pre → func() → post theo thứ tự
  • Từ bên ngoài, đây là greet mới
Điều @logger làm là đổi greet global thành một hàm khác gọi là wrapper. Thân greet gốc được gọi là func từ bên trong wrapper.

Xây một decorator bracket in lời chào trước và sau một hàm và xếp nó lên.

① Định nghĩa def bracket(func):, và bên trong def wrapper():. Cho wrapper chạy print("--- bắt đầu ---")func()print("--- kết thúc ---") theo thứ tự, và bên ngoài trả về wrapper.

② Định nghĩa def introduce(): được trang trí với @bracket, với chỉ print("Tôi là Minh") trong thân.

③ Gọi introduce() và xác nhận thân được kẹp giữa --- bắt đầu --- / --- kết thúc ---.

(Khi đáp án đúng, phần giải thích sẽ xuất hiện.)

Python Editor

Chạy code để xem đầu ra

Truyền bất kỳ tham số nào qua với *args / **kwargs

Đến giờ, wrapper không nhận tham số. Khi bạn muốn trang trí các hàm nhận tham số, dùng *args / **kwargs để nhận bất kỳ tham số nào nguyên dạng và chuyển tiếp thẳng tới `func`.

Điều đó biến decorator thành decorator chung hoạt động với mọi chữ ký hàm. Nó xử lý add(2, 3) (vị trí) và add(2, 3, name="ABC") (từ khóa) với cùng decorator.

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"call: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)   # unpack và chuyển tiếp ở đây cũng
        print(f"result: {result}")
        return result                    # đừng quên return nó
    return wrapper

@log_call
def add(a, b):
    return a + b

print(add(2, 3))
# call: args=(2, 3), kwargs={}
# result: 5
# 5

print(add(2, b=3))
# call: args=(2,), kwargs={'b': 3}
# result: 5
# 5
Cách *args / **kwargs truyền tham số thẳng qua
add(2, b=3)wrapper(*args, **kwargs)args=(2,), kwargs={'b': 3}unpack và chuyển tiếpthành func(*args, **kwargs)add(a, b)add(a=2, b=3) gốctrả về 5wrapper trả vềnguyên dạngnhậnpre-stepunpacktínhgửi kết quả về

Đừng quên return Kết quả

Nếu wrapper chỉ viết result = func(...) và quên return, giá trị trả về của hàm được trang trí âm thầm biến thành None. Tai nạn kinh điển là phát hiện add(2, 3) lặng lẽ trả về None — khi bạn viết decorator, coi return result như phần của cùng phản xạ cơ bắp.

Xây decorator log_call in mỗi lệnh gọi và áp dụng nó cho hàm 2 tham số.

① Định nghĩa def log_call(func):, và bên trong def wrapper(*args, **kwargs):.

② Trong wrapper, in f"call: args={args}, kwargs={kwargs}", rồi result = func(*args, **kwargs), rồi return result.

③ Bên ngoài trả về wrapper.

④ Định nghĩa def multiply(a, b): return a * b được trang trí với @log_call. Gọi print(multiply(4, 5))print(multiply(2, b=10)) và xác nhận log xuất hiện theo sau là giá trị trả về.

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Cái nào dưới đây có cùng nghĩa với việc đặt @logger phía trên def greet(): ...?

Câu 2Code này in gì?
def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * 2
return wrapper
@deco
def plus(a, b):
return a + b
print(plus(3, 4))