Decorator trong Python

Decorator được sử dụng tương đối nhiều trong Python. Ở bài viết này, Quantrimang.com sẽ cùng bạn tìm hiểu làm thế nào để tạo ra một Decorator và lý do tại sao bạn nên sử dụng nó. Hãy cùng đi tìm lời giải đáp! 

Decorator trong Python là gì?

Python có một tính năng khá thú vị gọi là decorator. 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ó.

Đây cũng được gọi là metaprogramming - siêu lập trình, hiểu đơn giản là "Code sinh ra code", nghĩa là mình viết một chương trình và chương trình này sẽ sinh ra, điều khiển các chương trình khác hoặc làm một phần công việc ngay tại thời điểm biên dịch. 

Điều kiện để có Decorator

Để hiểu về decorator, trước tiên bạn cần xem lại một vài điều cơ bản trong Python.

Hàm là một khái niệm rất cơ bản trong lập trình nói chung. Tuy nhiên, trong Python, hàm cũng là đối tượng. Các tên hàm mà chúng ta khai báo chỉ đơn giản là định danh ràng buộc với các đối tượng này. Một đối tượng hàm cũng có thể được liên kết cùng với nhiều tên khác nhau. Ví dụ:

def first(msg):
     print(msg)    
 
 first("Hello")
 
 second = first
 second("Hello")

Khi bạn chạy code, hàm firstsecond đều trả về cùng một output. Ở đây, firstsecond đề cập đến cùng một đối tượng hàm.

Hãy theo dõi tiếp, các hàm có thể được truyền dưới dạng tham số cho một hàm khác (tương tự như map, filterreduce trong Python).

Những hàm lấy hàm khác làm tham số đầu vào được gọi là hàm bậc cao (higher-order functions). Ví dụ như này:

def inc(x):
     return x + 1
 
 def dec(x):
     return x - 1
 
 def operate(func, x):
     result = func(x)
     return result

Chúng ta gọi hàm như sau.

>>> operate(inc,3)
 4
 >>> operate(dec,3)
 2

Hơn nữa, một hàm có thể trả về kết quả một hàm khác.

def is_called():
     def is_returned():
         print("Hello")
     return is_returned
 
 new = is_called()
 
 #Outputs "Hello"
 new()

Ở đây, is_returned() là một hàm lồng nhau, hàm này sẽ được truy cập và trả về kết quả mỗi khi ta gọi hàm is_called().

Để rõ hơn, bạn có thể tham khảo thêm bài học Cách sử dụng Closure trong Python

Quay trở lại với Decorator, hiểu một cách cơ bản nhất, Decorator là một hàm có thể nhận các hàm khác, 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 kết quả.

def make_pretty(func):
     def inner():
         print("I got decorated")
         func()
     return inner
 
 def ordinary():
     print("I am ordinary")

Chạy code trong Python shell:

>>> ordinary()
 I am ordinary
 
 >>> # Thử hàm decorate trong hàm ordinary
 >>> pretty = make_pretty(ordinary)
 >>> pretty()
 I got decorated
 I am ordinary

Trong ví dụ trên, make_pretty() là một decorator. Khi ta gọi

pretty = make_pretty(ordinary)
 

thì hàm ordinary() được decorator truyền vào làm tham số và hàm trả về tên là pretty.

Bạn có thể thấy ở đây, decorator đã thêm một hàm mới cho hàm ban đầu. Hãy hình dung nó như kiểu đóng gói một món quá. Các decorator là lớp bọc ở ngoài, bản chất của đối tượng được decorator truyền vào làm tham số (món quà bên trong) không thay đổi, nhưng hiện giờ nó có thêm một lớp bọc decorator ở ngoài.

Nói chung, ở đây ta decorator một hàm và gán lại nó:

ordinary = make_pretty(ordinary)

Đây là một cấu trúc phổ biến, vì vậy Python có một cú pháp để đơn giản hóa việc này.

Bạn có thể sử dụng ký hiệu @ cùng với tên của hàm decorator và đặt nó lên trên định nghĩa của hàm được decorator. Ví dụ:

@make_pretty
 def ordinary():
     print("I am ordinary")

tương đương với:

def ordinary():
     print("I am ordinary")
 ordinary = make_pretty(ordinary)

Đây là một cú pháp đặc biệt để thực hiện decorator.

Tham số hàm decorator

Các decorator ở trên đều khá đơn giản và hoạt động cùng với các hàm không có bất kỳ tham số nào. Bây giờ ta hãy thử truyền tham số cho hàm trả về bởi decorator như sau:

def divide(a, b):
     return a/b

Hàm này có hai tham số, a và b, sẽ báo lỗi nếu b=0

>>> divide(2,5)
 0.4
 >>> divide(2,0)
 Traceback (most recent call last):
 ...
 ZeroDivisionError: division by zero

Khi ta gọi hàm là thực ra gọi hàm được trả về bởi decorator, nên nếu truyền tham số cho hàm này thì nó sẽ truyền cho hàm được decorate.

Bây giờ chúng ta hãy tạo một decorator để kiểm tra trường hợp này có xảy ra lỗi hay không.

def smart_divide(func):
    def inner(a,b):
       print("I am going to divide",a,"and",b)
       if b == 0:
          print("Whoops! cannot divide")
          return
 
       return func(a,b)
    return inner
 
 @smart_divide
 def divide(a,b):
     return a/b

Chương trình sẽ trả về None nếu có lỗi phát sinh.

>>> divide(2,5)
 I am going to divide 2 and 5
 0.4
 >>> divide(2,0)
 I am going to divide 2 and 0
 Whoops! cannot divide

Đây chính là cách sử dụng hàm decorator có tham số.

Ngoài ra, bạn có thể xây dựng một decorator với số lượng tham số khác nhau tùy ý, chỉ cần sử dụng cú pháp *args**kwargs.

def works_for_all(func):
     def inner(*args, **kwargs):
         print("I can decorate any function")
         return func(*args, **kwargs)
     return inner

Khi ta gọi hàm, thực ra là chúng ta gọi hàm được trả về bởi decorator, nên nếu chúng ta truyền tham số cho hàm này thì nó sẽ truyền cho hàm được decorate.

Chuỗi Decorator trong Python

Nhiều decorator có thể được tạo thành chuỗi decorator trong Python.

Nghĩa là một hàm có thể được decorate nhiều lần với các decorator giống hoặc khác nhau, chỉ cần đặt decorator lên trước hàm bạn muốn là được.

def star(func):
     def inner(*args, **kwargs):
         print("*" * 30)
         func(*args, **kwargs)
         print("*" * 30)
     return inner
 
 def percent(func):
     def inner(*args, **kwargs):
         print("%" * 30)
         func(*args, **kwargs)
         print("%" * 30)
     return inner
 
 @star
 @percent
 def printer(msg):
     print(msg)
 printer("Hello")

Chương trình sẽ trả về output:

******************************
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 Hello
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 ******************************

Cú pháp:

@star
 @percent
 def printer(msg):
     print(msg)
 

tương đương với:

def printer(msg):
     print(msg)
 printer = star(percent(printer))

Thứ tự của decorator cũng quan trọng, nếu bạn đảo ngược: 

@percent
 @star
 def printer(msg):
     print(msg)

Kết quả sẽ khác:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 ******************************
 Hello
 ******************************
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Bài trước: Cách sử dụng Closure trong Python

Bài tiếp: Khai báo @property trong Python

Thứ Ba, 23/07/2019 10:01
53 👨 386