Python class decorator

Python class decorator
Photo by laura adai from Unsplash

Trong bài viết trước, chúng ta đã tìm hiểu về metaclass trong Python. Và đi đến kết luận rằng, metaclass là một khái niệm rất phức tạp của Python và chúng ta không nên lạm dụng nó. Nếu muốn thay đổi hoạt động của một class, chúng ta có thể sử dụng class decorator. Bài viết này sẽ giới thiệu cách viết và sử dụng class decorator trong Python.

Nhắc lại kiến thức

Cơ sở lý thuyết của class decorator cũng tương tự như function decorator, chúng đã được trình bày rất đầy đủ ở bài viết về function decorator. Trong bài viết này, chúng ta chỉ điểm qua một vài ý cơ bản.

Decorator cho hàm

Chúng biết được rằng, hàm của Python cũng là đối tượng, nên chúng ta có thể viết một hàm, nhận đầu vào là một hàm khác, thay đổi hoạt động của hàm đó, và sau đó lại trả về (return) một hàm. Đó chính là cơ sở lý thuyết để chúng ta xây dựng decorator cho các hàm.

>>> def decorator_function(function_to_decorate):
...     def wrapper():
...         print("Before calling function")
...         function_to_decorate()
...         print("After calling function")
...     return wrapper
...
>>> @decorator_function
...  def a_function():
...     print("Do not modify it")
...
>>> a_function()
Before calling function
Do not modify it
After calling function

Class cũng là đối tượng

Trong bài viết về metaclass, chúng ta đã biết rằng, class trong Python cũng là đối tượng. Vì cũng là đối tượng nên chúng ta có thể thao tác với chúng như những đối tượng thông thường khác: gán biến, thêm thuộc tính và truyền vào các hàm làm tham số.

>>> class Foo:
...     pass
...
>>> print(Foo)
<class '__main__.Foo'>
>>> Bar = Foo
>>> print(Bar)
<class '__main__.Foo'>
>>> print(Foo())
<__main__.Foo object at 0x7f85d0aa6b00>
>>> print(Bar())
<__main__.Foo object at 0x7f85d0aa6b38>

Decorator cho class

Với cơ sở lý thuyết như trên, chúng ta cũng có thể xây dựng các hàm, nhận đầu vào là một class, và thay đổi hành vi của class đó. Tuy nhiên, một class với các thuộc tính phức tạp hơn nên hàm này có thể trả về một class mới, hoặc một hàm mới, hoặc có thể trả về ngay class cũ.

Ví dụ, chúng ta muốn thêm một thuộc tính cho class, mà không muốn thay đổi code của class đó. Chúng ta có thể dùng một hàm như sau:

>>> def decorator_class(class_to_decorate):
...     class_to_decorate.decorator_attribute = "Decorator attribute"
...     return class_to_decorate
...
>>> Foo.decorator_attribute # Lúc này thuộc tính này chưa tồn tại
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Foo' has no attribute 'decorator_attribute'
>>> Foo = decorator_class(Foo) # Sau khi chạy hàm này, thuộc tính đã được thêm vào
>>> Foo.decorator_attribute
'Decorator attribute'

Cú pháp class decorator

Từ phiên bản Python 2.6, Python cho phép chúng ta sử dụng pie syntax (@) với class decorator, tương tự như decorator cho hàm.

Ví dụ ở trên có thể viết ngắn gọn lại như sau:

>>> def decorator_class(class_to_decorate):
...     class_to_decorate.decorator_attribute = "Decorator attribute"
...     return class_to_decorate
...
>>> @decorator_class
...  class Foo:
...     pass
...
>>> Foo
<class '__main__.Foo'>
>>> Foo.decorator_attribute
'Decorator attribute'

Một ví dụ khác, có thể chúng ta sẽ cần sử dụng nhiều, đó là cài đặt singleton pattern.

>>> def singleton(cls):
...     instance = None
...     def get_instance():
...         nonlocal instance
...         if not instance:
...             instance = cls()
...         return instance
...     return get_instance
...
>>> @singleton
...  class Foo:
...     pass
...
>>> x = Foo()
>>> id(x)
140212673042024
>>> y = Foo()
>>> id(y)
140212673042024
>>> id(x) == id(y)
True
>>> @singleton
...  class Bar:
...     pass
...
>>> a = Bar()
>>> b = Bar()
>>> id(a)
140212708208144
>>> id(b)
140212708208144
>>> id(a) == id(b)
True
>>> id(x) == id(a)
False

Trong ví dụ trên, chúng ta đã khởi tạo các giá trị của class FooBar hai lần, nhưng chúng chỉ tạo ra một instance cho mỗi class mà thôi.

Tính năng tương tự cũng có thể thực hiện được với metaclass, bằng cách override phương thức __call__ của metaclass, chúng ta có thể thay đổi quá trình tạo ra các instance.

>>> class Singleton(type):
...     _instances = {}
...     def __call__(cls, *args, **kwargs):
...         if cls not in cls._instances:
...             cls._instances[cls] = super().__call__(*args, **kwargs)
...         return cls._instances[cls]
...
>>> class Foo(metaclass=Singleton):
...     pass
...
>>> x = Foo()
>>> y = Foo()
>>> id(x)
140173760907024
>>> id(y)
140173760907024
>>> id(x) == id(y)
True

Thêm một ví dụ nữa về class decorator có phần khó hơn. Đó là chúng ta sẽ thay đổi một số phương thức của class. Thay vì decorator cho từng class một, chúng ta có thể decorate cả class.

>>> def method_decorator(method):
...     def wrapper(*args, **kwargs):
...         print("Inside the decorator")
...         return method(*args, **kwargs)
...     return wrapper
...
>>> def class_decorator(*method_names):
...     def class_rebuilder(cls):
...         class NewClass(cls):
...             def __getattribute__(self, attr_name):
...                 obj = super().__getattribute__(attr_name)
...                 if hasattr(obj, '__call__') and attr_name in method_names:
...                     return method_decorator(obj)
...                 return obj
...         return NewClass
...     return class_rebuilder
...
>>> @class_decorator('first_method', 'second_method')
...  class Foo:
...     def first_method(self):
...         print("\tThis is the first_method of Foo class")
...     def second_method(self):
...         print("\tThis is the second_method of Foo class")
...
>>> bar = Foo()
>>> bar.first_method()
Inside the decorator
    This is the first_method of Foo class
>>> bar.second_method()
Inside the decorator
    This is the second_method of Foo class

Trong trường hợp này, việc sử dụng metaclass để thay đổi các phương thức của class có vẻ không phải là một phương án dễ dàng. Việc sử dụng class decorator đã khiến một công việc khó khăn trở nên đơn giản hơn rất nhiều.

Một số hạn chế của class decorator

Class decorator tuy rằng cho phép chúng ta thay đổi hành vi của các class, nhưng nó cũng có những hạn chế nhất định:

  • Class decorator chỉ xuất hiện từ phiên bản 2.6. Nên những phiên bản cũ hơn chưa thể dùng được cú pháp pie syntax (@). Tất nhiên, bạn vẫn có thể làm bằng tay bằng cách gọi hàm.
  • Decorator không giúp cải thiện performance của các class. Thậm chí, nó còn làm class chậm hơn.
  • Cũng giống như một hàm đã được decorate, class được decorate cũng không thể bỏ decorate đi được.
  • Vì decorator bao bọc các class và kết quả trả về nhiều khi không còn là class, việc debug sẽ trở nên khó khăn hơn. Và class decorator không có công cụ nào có sẵn để giúp chúng ta việc này. (Không như functools giúp chúng ta debug các hàm.)

Kết luận

Trên đây là những hiểu biết của tôi về class decorator trong Python. Class decorator là một công cụ rất hữu ích và dễ hiểu hơn rất nhiều so với metaclass. Hy vọng bài viết giúp ích phần nào trong quá trình phát triển Python của các bạn.

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.