Tìm hiểu Python decorators

Decorator trong Python được sử dụng khá nhiều, tuy nhiên nó cũng hơi khó hiểu một chút. Decorator cho chúng ta một cú pháp đơn giản để gọi các hàm bậc cao (higher-order functions). Về mặt lý thuyết, một decorator là một hàm nhận tham số đầu vào là một hàm khác và mở rộng tính năng cho hàm đó mà không thay đổi nội dung của nó. Tuy nhiên, decorator không chỉ có vậy, nó thực sự còn làm nhiều việc khác nữa. Chúng ta sẽ dần dần khám phá trong bài viết này.
Trong bài viết này, tôi sẽ giới thiệu một cách ngắn gọn và dễ hiểu về decorator, cách sử dụng chúng và cách tự viết một decorator cho riêng mình.
Từ "decorator" được sử dụng trong Python có một chút biến đổi. Bạn hãy cẩn thận vì nó rất dễ gây nhầm lẫn với một design pattern cũng tên là decorator. Thực ra, chúng ta có thể sử dụng decorator trong Python để cài đặt decorator pattern. Nhưng có rất nhiều hạn chế trong cách làm này. Theo tôi thấy thì decorator của Python có nhiều nét giống với macro hơn.
Cơ sở lý thuyết
Trước khi bạn có thể hiểu các decorator, thì bạn cần hiểu một số khái niệm cơ bản của Python trước, trong phần này, chúng ta sẽ dần dần tìm hiểu những khái niệm đó.
Hàm
Hàm là một khái niệm rất cơ bản trong lập trình nói chung và Python nói riêng, nên tôi nghĩ không cần nói nhiều về nó. Đương nhiên các hàm sẽ trả về kết quả dựa trên tham số đầu vào của nó:
Tôi nghĩ không cần phải nói nhiều về khái niệm này nữa, nó rất dễ hiểu. Nên chúng ta sẽ chuyển sang khái niệm tiếp theo.
Hàm cũng là đối tượng
Trong Python, hàm cũng là đối tượng, hơn nữa nó còn là đối tượng first-class. Điều đó cho rất nhiều kết quả quan trọng. Chúng ta sẽ tìm hiểu thông qua một số ví dụ sau:
Các hàm lồng nhau
Vì hàm là các đối tượng first-class (thực ra trong Python, mọi thứ đều là đối tượng first-class) nên nó có một đặc tính khá thú vị là nó có thể được định nghĩa bên trong một hàm khác. Các hàm như vậy gọi là các hàm lồng nhau.
Bài học rút ra
Hàm là các đối tượng (hơn nữa là đối tượng first-class), vì vậy:
- Hàm có thể được gán cho các biến.
- Hàm có thể được định nghĩa trong một hàm khác.
Vì những đặc điểm trên hàm còn có thể trả kết quả (return
) là một hàm khác.
Một hàm trả kết quả là một hàm khác
Python cho phép bạn viết một hàm mà hàm đó trả kết quả về là một hàm khác. Ví dụ:
Không chỉ có thể trả kết quả là một hàm, hàm còn thể được truyền vào hàm khác như là một tham số.
Trên đây là những thứ cần thiết để hiểu về decorator của Python. Bạn sẽ thấy rằng, decorator là "wrapper" cho phép bạn chạy một số đoạn code trước hoặc sau hàm chính mà không thay đổi nội dung của nó.
Python decorator cơ bản
Khi có một hàm mà bạn muốn chạy một số code trước, hoặc sau hàm đó, bạn sẽ làm thế nào?
Làm bằng tay
Chúng ta có thể sử dụng kiến thức về các hàm lồng nhau ở trên, để giải quyết bài toán này.
Decorator
Chúng có thể sử dụng cú pháp decorator của Python với ví dụ trên như sau:
Rất dễ hiểu phải không? Cú pháp @decorator
(còn gọi là pie syntax) là cách viết ngắn gọn của:
Thậm chí, bạn có thể sử dụng nhiều decorator cho một hàm, bởi kết quả trả về của decorator cũng là một hàm và nó hoàn toàn có thể được decorate bởi những decorator khác.
Sử dụng cú pháp decorator của Python
Python decorator nâng cao
Truyền tham số vào hàm được decorate
Decorate một phương thức
Hàm và phương thức của Python rất giống nhau, ngoại trừ một điểm, phương thức thuộc về một class mà nó luôn luôn có tham số đầu tiên là self
(đối tượng hiện tại).
Vì vậy, chúng ta có thể viết decorator cho phương thức giống như decorator cho hàm, chỉ với lưu ý rằng cần truyền tham số self
cho wrapper.
Decorator tổng quát
Nếu bạn muốn xây dựng một decorator một cách tổng quát, có thể áp dụng cho mọi hàm với số lượng tham số khác nhau. Bạn chỉ cần sử dụng cú pháp *args
và **kwargs
Truyền tham số cho chính decorator
Bây giờ, nếu bạn muốn truyền tham số cho chính decorator thì phải làm thế nào? Bởi vì decorator nhận tham số là một hàm nên bạn không thể truyền tham số cho nó được.
Trước khi đến với giải pháp, chúng ta xem qua ví dụ sau:
Hai cách làm cho kết quả giống hệt nhau. Đó là bởi vì khi bạn sử dụng cú pháp @
thì Python sẽ gọi và thực thi hàm có tên là tên của decorator như thực thi một hàm thông thường.
Điều này rất quan trọng, bởi vì bạn có thể dùng tên của decorator để trỏ tới chính hàm decorator đó hay không.
Chúng ta có thể làm điều tương tự bằng cách bỏ qua các biến trung gian
Thậm chí chúng ta có thể dùng cú pháp ngắn gọn hơn:
Rất đơn giản, chúng ta có thể gọi và thực thi hàm trong chính cú pháp @
.
Quay trở lại với bài toán của chúng ta, nếu chúng ta xây dựng một hàm trả kết quả về là một decorator thì chúng ta có thể truyền tham số cho hàm đó.
Việc truyền tham số vào các hàm lồng nhau như vậy có sự giúp đỡ của closure. Bạn có thể tìm hiểu thêm về closure ở bài viết này
Vậy là chúng ta đã có một hàm, và chúng ta có thể truyền tham số cho hàm đó và sử dụng nó làm decorator. Thậm chí chúng ta có thể dùng biến để truyền tham số.
Với cách làm như trên, bạn có thể xây dựng decorator nhận tham số như những hàm thông thường khác. Bạn cũng có thể sử dụng *args
và **kwargs
nếu muốn. Tuy nhiên, cần lưu ý rằng, decorator chỉ được gọi duy nhất một lần. Khi một hàm đã được decorate rồi, bạn không thể thay đổi nó nữa. Bạn cũng không thể truyền tham số động được.
Decorate một decorator
Trong phần trước, tôi đã trình bày cách xây dựng một decorator nhận tham số đầu vào. Biện pháp là sử dụng thêm một hàm lồng nhau nữa. Tức là chúng ta đã bao bọc decorator bằng một hàm khác. Nghe quen thế nhỉ? Chính xác, đây chính là cách xây dựng một decorator.
Đây là một trick khá thú vị, decorate một decorator. Bởi vì decorator cũng là hàm nên nó hoàn toàn có thể được decorate bởi những decorator khác.
Chúng ta có thể sử dụng nó như sau:
Lưu ý
- Decorator được giới thiệu từ phiên bản Python 2.4 nên bạn cần sử dụng Python đúng phiên bản mới có thể dùng được.
- Mặc dù cú pháp đơn giản, nhưng decorator không làm hàm nhanh hơn, thậm chí nó còn làm hàm chậm đi.
- Bạn không thể bỏ decorator cho một hàm. Một khi hàm đã được decorate rồi, thì nó sẽ được decorate mãi mãi.
- Vì decorator bao bọc các hàm nên rất khó để debug. Tuy nhiên, đã có giải pháp nếu bạn sử dụng Python 2.5 trở nên.
Từ phiên bản 2.5 trở nên, Python có module functools
trong đó có hàm functools.wraps
giúp chúng ta copy tên, module, docstring, v.v. của hàm được decorate vào hàm wrapper của chúng ta.
Decorator có thực sự cần thiết hay không?
Tất nhiên là cần thiết thì người ta mới có cú pháp cho decorator. Tuy nhiên, câu hỏi đặt ra là khi nào thì sử dụng decorator.
Decorator có thể được sử dụng để debug mà không cần thay đổi hàm hay thư viện. Ví dụ chúng ta có thể dùng decorator để debug và đánh giá hiệu năng các tính năng mà không cần thay đổi code của chúng.
Điều tuyệt vời của decorator là bạn có thể dùng nó với bất cứ thứ gì mà không cần thay đổi nội dung.
Python bản thân nó cũng có sẵn rất nhiều decorator như @property
, @classmethod
, v.v... Django cũng thường xuyên sử dụng decorator, nhất là khi phân quyền cho người dùng.
Decorator là một công cụ rất mạnh mẽ, nên nếu sử dụng đúng, nó sẽ cho chúng ta những tiện ích tuyệt vời.
Bonus: Decorator mà vẫn giữ được hàm gốc
Như phần trước tôi đã nói, một hàm khi đã được decorate rồi thì chúng ta không thể bỏ decorator đi được. Bất cứ khi nào bạn gọi hàm, thực ra bạn đang gọi hàm đã được decorate.
Nhưng nhiều trường hợp, bạn chỉ muốn thực thi hàm gốc thôi. Vậy phải làm thế nào? Không lẽ phải định nghĩa lại hàm.
Thực ra có một trick để làm việc này. Nói chung nó rất dễ hiểu nên chắc chỉ cần nhìn code là hiểu ngay.
Có một cách khác, đó là sử dụng __closure__
để lấy lại nội dung của hàm gốc. Cách này chỉ hoạt động nếu chúng ta có sử dụng closure. Lưu ý rằng, không phải tất cả các hàm lồng nhau đều là closure.
Kết luận
Trên đây là những hiểu biết của tôi về decorator được dùng trong Python. Decorator là một công cụ rất hữu dụng và được sử dụng khá thường xuyên. Hy vọng bài viết cho bạn phần nào hiểu biết về decorator, cách viết và sử dụng chúng.
Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.
Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.
Welcome

Xin chào. Tôi là manhhomienbienthuy, nickname khác là naa. Đây là thế giới của tôi, chào mừng đến với thế giới của tôi…
Bài viết liên quan
Bài viết mới
Chuyên mục
Lưu trữ theo năm
Câu nói yêu thích
There's a big difference between knowing the name of something and knowing something.
– Richard P. Feynman –
Thông tin liên hệ
Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.