Decorator trong Python
Decorator là một công cụ rất mạnh mẽ và hữu ích trong Python vì nó cho phép các lập trình viên sửa đổi hành vi của hàm hoặc lớp.
Bạn đã từng gặp loại cú pháp đặc biệt @classmethod
, @staticmethod
, @property
khi xây dựng class. Trong Python, lại cú pháp này là một cách sử dụng của decorator. Decorator là một hàm nhận hàm khác làm tham số để mở rộng khả năng của hàm tham số (nhưng không cần điều chỉnh thân hàm tham số). Bài học này sẽ trình bày chi tiết các vấn đề liên quan đến việc xây dựng và sử dụng decorator trong Python.
Decorator trong Python là gì?
Decorator là một mẫu thiết kế trong Python cho phép người dùng thêm chức năng mới vào một đối tượng hiện có mà không cần sửa đổi cấu trúc của nó. Đây là một trong số các mẫu thiết kế cổ điển.
Trong Python mẫu thiết decorator được hỗ trợ và sử dụng rộng rãi.
Khi học về phương thức (method) và thuộc tính (property) trong class Python bạn đã gặp một số cách viết lạ như @classmethod
, @staticmethod
, @property
. Chúng được gọi là những decorator (hàm trang trí).
Decorator trong Python là những hàm nhận hàm khác làm tham số để mở rộng khả năng của hàm tham số (nhưng không cần điều chỉnh thân hàm tham số).
Hàm trang trí trong Python chính là một vận dụng của mẫu thiết kế decorator.
Vậy tại sao lại cần decorator trong Python?
Trong quá trình xây dựng hàm bạn có thể sẽ gặp những yêu cầu chung lặp đi lặp lại. Lấy ví dụ, trong một số trường hợp bạn sẽ muốn ghi chú (log) lại quá trình thực hiện của một hàm để theo dõi và debug. Bạn có thể cần khởi tạo một số tài nguyên trước khi hàm có thể hoạt động. Bạn có thể muốn dọn dẹp object sau khi một hàm kết thúc. Bạn có thể muốn đo thời gian thực hiện của một hàm bằng cách tính khoảng thời gian từ lúc bắt đầu đến lúc kết thúc thực hiện hàm.
Dĩ nhiên, bạn có thể viết hết các logic trên vào trong hàm! Tuy nhiên, hãy nghĩ tới tình huống bạn có rất nhiều hàm phải xử lý tương tự nhau. Liệu bạn có muốn copy một đoạn code tới tất cả các hàm cần xử lý?
Khi đó, decorator sẽ giúp bạn bổ sung các tính năng cần thiết vào một hàm mà không cần viết thêm code cho hàm đó. Bạn có thể giữ hàm gốc với các tính năng cơ bản của nó.
Về kỹ thuật, decorator trong Python thực chất là một hàm nhận một hàm khác làm tham số và trả lại kết quả cũng là một hàm. Python có một số hỗ trợ giúp đơn giản hóa việc sử dụng hàm decorator.
Các vấn đề như “hàm làm tham số” và “hàm làm kết quả” có thể hơi khó hiểu. Chúng ta sẽ lân lượt giải thích từng vấn đề qua các phần tiếp theo.
Hàm bậc cao
Trong Python, bạn có thể truyền một hàm làm tham số của một hàm khác.
Ghi chú: truyền hàm làm tham số không giống truyền lời gọi hàm. Khi truyền hàm làm tham số, bạn chỉ truyền tên hàm. Trong thân hàm chính có thể gọi hàm tham số như một hàm thông thường.
Hãy xem ví dụ sau đây:
Ví dụ
def hello(name):
print(f'Hello, {name}. Welcome to heaven!')
def hi(name):
print(f'Hi, {name}. Welcome to hell!')
def greeting(name, func):
print('Inside high order function:')
func(name)
print('---')
greeting('Donald Trump', hello)
greeting('Vladimir Putin', hi)
Trong ví dụ trên, hi và hello là hai hàm bình thường. Các hàm này nhận một chuỗi tên người và in ra lời chào.
Trong khi đó, greeting()
lại là một hàm bậc cao: nó nhận hai tham số (name
và func
). Qua hình thức header, greeting() không khác gì so với hi() hay hello(). Tuy nhiên, khi nhìn vào thân hàm, bạn để ý dòng lệnh func(name)
. Đây là lời gọi hàm chứ không phải là lệnh truy xuất giá trị.
Như vậy, tham số func
của greeting()
không phải là một giá trị mà là một hàm. Hàm tham số này có thể được sử dụng bình thường bên trong thân hàm chính.
Một hàm nhận một hàm khác làm tham số được gọi là hàm bậc cao.
Hàm lồng nhau và closure
Trong Python, một hàm có thể trả kết quả là một hàm khác. Trả lại một hàm làm kết quả có nghĩa là bạn có thể gọi hàm kết quả đó như một hàm khai báo theo kiểu bình thường thông thường.
Hãy xem ví dụ sau:
Ví dụ
def hello(name):
"đây là một hàm bình thường"
print(f'Hello, {name}. Welcome to heaven!')
def welcome():
"đây là một hàm bậc cao, kết quả trả về là một hàm khác"
return hello
# lời gọi hàm welcome() sẽ tạo ra một hàm mới xxx tương đương với hàm hello()
xxx = welcome()
# xxx có thể xem như là một tên gọi khác của hello()
xxx('Obama')
Trong ví dụ này, hello() là một hàm thông thường in ra lời chào.
Tuy nhiên, welcome() lại là một hàm đặc biệt: kết quả trả về của nó không phải là một giá trị mà là một hàm đã khai báo, hàm hello().
Khi đó lời gọi hàm welcome() sẽ tạo ra một hàm mới xxx tương đương với hàm hello(). Cũng có thể xem xxx như là một tên gọi khác của hello(). Bạn sử dụng hàm kết quả xxx() giống hệt như sử dụng hello().
Trong Python, bạn có thể khai báo một hàm bên trong hàm khác. Hàm khai báo trong một hàm khác được gọi là nested function (hàm lồng nhau).
Hãy cùng xem một ví dụ khác:
Ví dụ
def welcome(greeting: 'lời chào'):
def hello(name: 'tên người'):
print(f'{greeting}, {name}. Welcome to heaven!')
return hello
hi = welcome('Hello')
hi('Putin')
Lần này thay vì sử dụng một hàm hello() xây dựng trước, chúng ta tự định nghĩa một hàm hello() mới nằm trong welcome(). Hàm hello() là hàm lồng (hàm con) của welcome().
Việc khai báo hàm hello() trong hàm welcome() và trả hàm hello() làm kết quả của welcome() được gọi là closure.
Closure là một hàm khai báo bên trong một hàm khác nhưng sau đó được truyền ra ngoài hàm chứa nó (và có thể được sử dụng bởi code bên ngoài hàm chứa).
Điểm đặc biệt của closure là hàm lồng có thể sử dụng biến và tham số của hàm chứa nó. Trong ví dụ trên, hello() sử dụng được tham số greeting của welcome().
Xây dựng hàm decorator trong Python
Qua hai phần trên bạn đã hiểu các yếu tố cơ bản tạo ra decorator. Giờ chúng ta cùng vận dụng để tự mình tạo ra một hàm decorator mới.
Hãy cùng thực hiện một ví dụ:
Ví dụ
def display_decorator(func):
def wrapper(str):
# logic trước khi chạy hàm func
print(f'Log: The function {func.__name__} is executing ...')
func(str)
# logic sau khi chạy hàm func
print('Log: Execution completed.\n')
return wrapper
def display(str):
print(str)
display = display_decorator(display)
display('Hello world')
@display_decorator
def say_hello(str):
print(str)
say_hello('Hello, Donald')
Trong ví dụ trên bạn đã xây dựng hàm bậc cao display_decorator nhận một hàm khác làm tham số. Yêu cầu của hàm tham số là nhận một chuỗi tham số.
Trong hàm display_decorator bạn tạo một closure wrapper(). Trong wrapper() bạn thực hiện gọi hàm func(), thêm một số logic trước và sau khi chạy func. Điều đặc biệt là wrapper() cần có chung danh sách tham số với func().
__name__
là attribute chứa tên của đối tượng. Ở đây chúng ta sử dụng __name__
để lấy tên chính thức của hàm tham số func.
Hàm display_decorator trả lại wrapper() làm kết quả.
Trong mẫu decorator, wrapper() chính là mấu chốt vấn đề: để mở rộng tính năng của một hàm func(), bạn sẽ thay func() bằng wrapper(). Điều này thể hiện rõ ở cách sử dụng cơ bản của decorator:
Ví dụ
def display(str):
print(str)
display = display_decorator(display) # thay display() bằng wrapper() nhưng gán trở lại tên display
display('Hello world') # hàm vẫn có tên display() nhưng thực tế chính là wrapper()
Để đơn giản hóa sử dụng hàm decorator, Python cung cấp cú pháp @. Cú pháp này giúp gộp lệnh gọi decorator với lệnh khai báo hàm:
Ví dụ
@display_decorator
def say_hello(str):
print(str)
say_hello('Hello, Donald')
Hai cách sử dụng decorator là tương đương nhau.
Kết quả chạy chương trình như sau:
Ví dụ
Log: The function display is executing ...
Hello world
Log: Execution completed.
Log: The function say_hello is executing ...
Hello, Donald
Log: Execution completed.
Để ý thấy rằng chức năng chính của hàm display() hay say_hello() đều chỉ là in ra một chuỗi văn bản. Khi được đánh dấu với decorator, có thêm logic tự động thực hiện trước và sau khi thực thi hàm.
Một số vấn đề khác với decorator
Ở trên chúng ta đã nói về hàm decorator: một hàm nhận một hàm làm tham số và trả về một hàm kết quả.
Python còn mở rộng decorator thành lớp decorator. Nghĩa là bạn có thể xây dựng cả một class hoạt động với vai trò decorator cho hàm. Hãy xem ví dụ sau:
Ví dụ
class decoclass(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
# logic trước khi gọi hàm f
print('decorator initialised')
self.f(*args, **kwargs)
print('decorator terminated')
# logic sau khi gọi hàm f
@decoclass
def hello(name):
print(f'Hello, {name}. Welcome to heaven!')
hello('Obama')
Kết quả thực hiện của hàm hello(‘Obama’) sẽ là
Ví dụ
decorator initialised
Hello, Obama. Welcome to heaven!
decorator terminated
Trong ví dụ này, Python sẽ tạo object của decoclass và truyền hàm hello làm tham số khi khởi tạo. Phương thức magic __call__()
giúp class hoạt động (được gọi) như một hàm (chính xác hơn, một callable object – loại object có thể được gọi giống như hàm).
Nếu logic của bạn quá phức tạp và không thể trình bày trong một hàm, có thể bạn sẽ cần đến lớp decorator.
Nếu hàm wrapper() của bạn đơn giản, bạn có thể sử dụng hàm lambda.
Hàm lambda là loại hàm không có tên và phần thân chỉ được chứa một lệnh duy nhất. Hàm lambda được khai báo với cú pháp như sau:
Ví dụ
lambda tham_số: biểu_thức
Ví dụ
x = lambda a : a + 10 # lệnh gọi hàm này sẽ là x(5)
x = lambda a, b : a * b # lệnh gọi hàm sẽ là x(5, 6)
Bạn có thế sử dụng hàm lambda làm wrapper cho decorator như sau:
Ví dụ
def makebold(f):
return lambda: "<b>" + f() + "</b>"
@makebold
def say():
return "Hello"
print(say()) #// kết quả là '<b>Hello</b>'
Bạn cũng có thể ghép các decorator lại với nhau thành chuỗi, gọi là chaining decorators. Hãy xem ví dụ sau:
Ví dụ
def makebold(f):
return lambda: "<b>" + f() + "</b>"
def makeitalic(f):
return lambda: "<i>" + f() + "</i>"
@makebold
@makeitalic
def say():
return "Hello"
print(say()) #// kết quả là <b><i>Hello</i></b>
Kết luận
Trong bài học này bạn đã học chi tiết về decorator trong Python:
- Decorator trong Python là vận dụng của mẫu thiết kế decorator.
- Python cung cấp hàm decorator và lớp decorator giúp mở rộng chức năng của một hàm.
- Decorator là một hàm cấp cao, nhận một hàm làm tham số và trả về một hàm khác.
- Python cung cấp cú pháp tắt @ để đơn giản hóa sử dụng decorator.
- Có thể ghép nối các decorator lại với nhau.