Khi mới nhìn thấy thuật ngữ Metaprogramming, chắc hẳn bạn sẽ cảm thấy nó có vẻ rất mới mẻ và xa lạ, nhưng nếu bạn đã từng làm việc với các decorators hoặc các metaclasses, thì khi đó bạn đã thực hành metaprogramming rồi đó. Có thể nói rằng, trong metaprogramming thì code này sẽ điều chỉnh/xử lý code khác.

Trong bài học này chúng ta sẽ tìm hiểu về các Metaclasses, tại sao và khi nào nên sử dụng chúng, và các lựa chọn thay thế là gì. Đây là một chủ đề khá nâng cao trong Python, bạn nên nắm được hai kiến thức sau để có thể hiểu rõ hơn bài học này:

– Lập trình hướng đối tượng trong Python.

– Decorator trong Python

Lưu ý: Phần kiến thức trong bài này được áp dụng từ Python 3.3 trở lên

1. Metaclass

Trong Python, mọi thứ đều được liên kết với một số kiểu dữ liệu cụ thể. Ví dụ, nếu chúng ta có một biến chứa giá trị integer, vậy thì biến đó sẽ thuộc kiểu int. Bạn có thể biết được kiểu dữ liệu của bất cứ thành phần nào nhờ hàm type().

# -----------------------------------------------------------
#Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
#@author cafedevn
#Contact: cafedevn@gmail.com
#Fanpage: https://www.facebook.com/cafedevn
#Group: https://www.facebook.com/groups/cafedev.vn/
#Instagram: https://instagram.com/cafedevn
#Twitter: https://twitter.com/CafedeVn
#Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
#Pinterest: https://www.pinterest.com/cafedevvn/
#YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
# -----------------------------------------------------------

num = 23
print("Type of num is:", type(num)) 
  
lst = [1, 2, 4] 
print("Type of lst is:", type(lst)) 
  
name = "Atul"
print("Type of name is:", type(name)) 

Kết quả in ra là:

Type of num is: <class 'int'>
Type of lst is: <class 'list'>
Type of name is: <class 'str'>

Mọi kiểu dữ liệu trong Python đều được cài đặt dựa trên các lớp (class). Vì vậy, trong ví dụ trên, không giống như C hay Java mà tại đó int, char, và float là các kiểu dữ liệu chính, thì trong Python chúng đều là đối tượng, đối tượng của lớp int, hoặc đối tượng của lớp str. Vì vậy chúng ta có thể tạo ra một kiểu dữ liệu mới bằng cách tạo ra một lớp (class) của kiểu dữ liệu đó. Ví dụ, chúng ta có thể tạo ra một kiểu dữ liệu Student bằng cách tạo ra lớp Student:


class Student: 
    pass
stu_obj = Student() 
  
# Print type of object of Student class 
print("Type of stu_obj is:", type(stu_obj)) 

Kết quả in ra là:

Type of stu_obj is: <class '__main__.Student'>

Bản thân một class cũng chính là một đối tượng, và cũng giống như bất kỳ đối tượng nào khác, nó là một thể hiện của một thứ gọi là Metaclass. Một class đặc biệt có tên là type đã tạo ra các đối tượng Class này. class type là một metaclass mặc định chịu trách nhiệm tạo ra các class. Ví dụ, trong đoạn chương trình ở phía trên nếu chúng ta thử tìm hiểu kiểu dữ liệu của class Student, ta sẽ thấy được nó thuộc kiểu type.


class Student: 
    pass
  
# Print type of Student class 
print("Type of Student class is:", type(Student)) 

Kết quả in ra là:

Type of Student class is: <class 'type'>

Bởi vì các class cũng đều là các đối tượng (object), nên chúng có thể được chỉnh sửa theo cùng một cách. Chúng ta có thể thêm hoặc bớt đi các trường hoặc các phương thức bên trong một class theo cùng một cách mà chúng ta đã làm với các đối tượng khác. Ví dụ:

# -----------------------------------------------------------
#Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
#@author cafedevn
#Contact: cafedevn@gmail.com
#Fanpage: https://www.facebook.com/cafedevn
#Group: https://www.facebook.com/groups/cafedev.vn/
#Instagram: https://instagram.com/cafedevn
#Twitter: https://twitter.com/CafedeVn
#Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
#Pinterest: https://www.pinterest.com/cafedevvn/
#YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
# -----------------------------------------------------------

# Defined class without any 
# class methods and variables 
class test:pass
  
# Defining method variables 
test.x = 45
  
# Defining class methods 
test.foo = lambda self: print('Hello') 
  
# creating object 
myobj = test() 
  
print(myobj.x) 
myobj.foo() 

Kết quả in ra là:

45
Hello

Có thể tóm tắt quá trình này như sau: Các metaclass tạo ra các class, rồi các class lại tạo ra các object (đối tượng).

Metaclass chịu trách nhiệm tạo ra các classes, vì vậy chúng ta có thể tự viết được metaclass tùy chỉnh của riêng mình để thay đổi cách mà các classes được tạo ra, bằng cách thực hiện các hành động bổ sung, hoặc injecting code – tiêm mã. Thường thì chúng ta sẽ không cần đến các metaclasses tùy chỉnh, nhưng đôi khi chúng khá cần thiết.

Có những bài toàn mà cả các giải pháp dựa trên metaclass và dựa trên các non-metaclass đều khả dụng (thường đơn giản hơn), nhưng trong một số trường hợp, chỉ các metaclass mới có thể giải quyết được vấn đề. Chúng ta sẽ thảo luận vấn đề đó trong bài này.

2. Tạo ra các custom Metaclass – Metaclass tùy chỉnh

Để tạo ra một metaclass tùy chỉnh, metaclass tùy chỉnh của chúng ta cần kế thừa từ metaclass type, và thường ghi đè (override) các phương thức sau:

– __new__(): Phương thức này được gọi trước phương thức __init()__. Nó tạo ra một đối tượng và trả về nó. Chúng ta có thể ghi đè (override) lại phương thức này để kiểm soát cách mà đối tượng được tạo ra.

– __init__(): Phương thức này sẽ khởi tạo cái đối tượng đã được tạo ra, cái mà được truyền vào làm tham số.

Chúng ta có thể tạo ra các classes bằng cách sử dụng trực tiếp hàm type(). Nó có thể được gọi đến theo những cách sau:

– Khi  được gọi chỉ với một đối số, nó sẽ trả về kiểu dữ liệu, chúng ta đã từng thấy điều này trong các ví dụ trên.

– Khi được gọi với 3 tham số, nó sẽ tạo ra một class. Các đối số được truyền vào cho hàm type() trong trường hợp này là:

+ Class name – tên lớp

+ Tuple trong đó các base classes (lớp cha. lớp cơ sở) được kế thừa bởi các class

+ Class Dictionary: Nó phục vụ như một namespace cục bộ dành cho class, được tổ chức bởi các class methods (phương thức thuộc về class) và các biến.

Cùng xét ví dụ sau:

# -----------------------------------------------------------
#Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
#@author cafedevn
#Contact: cafedevn@gmail.com
#Fanpage: https://www.facebook.com/cafedevn
#Group: https://www.facebook.com/groups/cafedev.vn/
#Instagram: https://instagram.com/cafedevn
#Twitter: https://twitter.com/CafedeVn
#Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
#Pinterest: https://www.pinterest.com/cafedevvn/
#YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
# -----------------------------------------------------------

def test_method(self): 
    print("This is Test class method!") 
  
# creating a base class  
class Base: 
    def myfun(self): 
        print("This is inherited method!") 
  
# Creating Test class dynamically using 
# type() method directly 
Test = type('Test', (Base, ), dict(x="atul", my_method=test_method)) 
  
# Print type of Test  
print("Type of Test class: ", type(Test)) 
  
# Creating instance of Test class 
test_obj = Test() 
print("Type of test_obj: ", type(test_obj)) 
  
# calling inherited method 
test_obj.myfun() 
  
# calling Test class method 
test_obj.my_method() 
  
# printing variable 
print(test_obj.x) 

Kết quả in ra là:

Type of Test class:  <class 'type'>
Type of test_obj:  <class '__main__.Test'>
This is inherited method!
This is Test class method!
atul

Nhưng nếu chúng ta muốn tạo ra một metaclass mà không trực tiếp sử dụng hàm type() thì sao? Trong ví dụ sau đây, chúng ta sẽ tạo ra một metaclass tên là MultiBases, nó sẽ kiểm tra xem class đang được tạo ra có kế thừa từ nhiều hơn một base class (lớp cha) hay không. Nếu đúng như vậy, nó sẽ đưa ra một thông báo lỗi.


# our metaclass 
class MultiBases(type): 
    # overriding __new__ method 
    def __new__(cls, clsname, bases, clsdict): 
        # if no of base classes is greator than 1 
        # raise error 
        if len(bases)>1: 
            raise TypeError("Inherited multiple base classes!!!") 
          
        # else execute __new__ method of super class, ie. 
        # call __init__ of type class 
        return super().__new__(cls, clsname, bases, clsdict) 
  
# metaclass can be specified by 'metaclass' keyword argument 
# now MultiBase class is used for creating classes 
# this will be propagated to all subclasses of Base 
class Base(metaclass=MultiBases): 
    pass
  
# no error is raised 
class A(Base): 
    pass
  
# no error is raised 
class B(Base): 
    pass
  
# This will raise an error! 
class C(A, B): 
    pass

Kết quả in ra là:

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 8, in __new__
TypeError: Inherited multiple base classes!!!

3. Sử dụng metaclass để giải quyết các bài toán

Có một số bài toán/vấn đề có thể được giải quyết bởi các decorators (một cách đơn giản), cũng như bởi các metaclasses. Nhưng vẫn tồn tại một vài bài toán/vấn đề mà kết quả của nó chỉ có thể đạt được nhờ các metaclasses. Giả dụ như vấn đề lặp lại các đoạn code tương tự nhau trong khi lập trình.

Để debug các class methods (các phương thức thuộc về lớp), chúng ta sẽ code sao cho, mỗi khi class method nào thực thi, sẽ in ra tên đầy đủ của nó, trước khi thực thi nó.

Có lẽ, giải pháp đầu tiên mà chúng ta đều nghĩ đến chính là sử dụng các method decorators (các decorators dành cho phương thức), dưới đây là code ví dụ cho giải pháp này:

# -----------------------------------------------------------
#Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
#@author cafedevn
#Contact: cafedevn@gmail.com
#Fanpage: https://www.facebook.com/cafedevn
#Group: https://www.facebook.com/groups/cafedev.vn/
#Instagram: https://instagram.com/cafedevn
#Twitter: https://twitter.com/CafedeVn
#Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
#Pinterest: https://www.pinterest.com/cafedevvn/
#YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
# -----------------------------------------------------------

from functools import wraps 
  
def debug(func): 
    '''decorator for debugging passed function'''
      
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        print("Full name of this method:", func.__qualname__) 
        return func(*args, **kwargs) 
    return wrapper 
  
def debugmethods(cls): 
    '''class decorator make use of debug decorator 
       to debug class methods '''
      
    # check in class dictionary for any callable(method) 
    # if exist, replace it with debugged version 
    for key, val in vars(cls).items(): 
        if callable(val): 
            setattr(cls, key, debug(val)) 
    return cls
  
# sample class 
@debugmethods
class Calc: 
    def add(self, x, y): 
        return x+y 
    def mul(self, x, y): 
        return x*y 
    def div(self, x, y): 
        return x/y 
      
mycal = Calc() 
print(mycal.add(2, 3)) 
print(mycal.mul(5, 2)) 

Kết quả in ra là:

Full name of this method: Calc.add
5
Full name of this method: Calc.mul
10

Giải pháp này hoạt động ổn, nhưng vẫn có một vấn đề, sẽ thế nào nếu chúng ta muốn áp dụng cái method decorator này cho tất cả các subclasses (các lớp con) mà kế thừa từ class Calc. Trong trường hợp đó, chúng ta phải áp dụng một cách riêng rẽ cái method decorator cho từng subclass (lớp con) như khi chúng ta đã làm với class Calc.

Tuy nhiên, vấn đề ở đây là nếu chúng ta có rất nhiều subclasses, thì việc thêm decorator cho từng subclass riêng biệt hết sức bất tiện. Nếu chúng ta biết trước được rằng mỗi subclass đều phải có cái debug property – thuộc tính dành cho việc gỡ lỗi phần mềm này, thì chúng ta tốt hơn hết nên hướng đến giải pháp dựa trên metaclass.

Hãy xem giải pháp dựa trên metaclass ngay bên dưới đây, ý tưởng ở đây là các classes sẽ được tạo ra một cách bình thường, rồi sau đó ngay lập tức được đói gói bởi debug method decorator (cái decorator dành cho hàm, để phục vụ cho việc debug).


from functools import wraps 
  
def debug(func): 
    '''decorator for debugging passed function'''
      
    @wraps(func) 
    def wrapper(*args, **kwargs): 
        print("Full name of this method:", func.__qualname__) 
        return func(*args, **kwargs) 
    return wrapper 
  
def debugmethods(cls): 
    '''class decorator make use of debug decorator 
       to debug class methods '''
      
    for key, val in vars(cls).items(): 
        if callable(val): 
            setattr(cls, key, debug(val)) 
    return cls
  
class debugMeta(type): 
    '''meta class which feed created class object 
       to debugmethod to get debug functionality 
       enabled objects'''
      
    def __new__(cls, clsname, bases, clsdict): 
        obj = super().__new__(cls, clsname, bases, clsdict) 
        obj = debugmethods(obj) 
        return obj 
      
# base class with metaclass 'debugMeta' 
# now all the subclass of this  
# will have debugging applied 
class Base(metaclass=debugMeta):pass
  
# inheriting Base 
class Calc(Base): 
    def add(self, x, y): 
        return x+y 
      
# inheriting Calc 
class Calc_adv(Calc): 
    def mul(self, x, y): 
        return x*y 
  
# Now Calc_adv object showing 
# debugging behaviour 
mycal = Calc_adv() 
print(mycal.mul(2, 3)) 

Kết quả in ra là:

Full name of this method: Calc_adv.mul
6

4. Khi nào nên sử dụng Metaclass

Hầu hết thời gian chúng ta sẽ không sử dụng các metaclasses, chúng giống như là ma thuật đen vậy, và thường được coi là một thứ gì đó hết sức phức tạp, nhưng vẫn có một số trường hợp mà chúng ta sử dụng đến các metaclasses, đó là:

– Như chúng ta đã thấy trong ví dụ trên, các metaclasses sẽ lan truyền từ trên xuống dưới trong hệ thống phân cấp thừa kế (inheritance hierachy). Do đó nó cũng sẽ ảnh hưởng đến tất cả các lớp con. Nếu gặp trường hợp như vậy, chúng ta nên sử dụng các metaclasses.

– Khi chúng ta muốn thay đổi các classes một cách tự động, khi nó được tạo ra.

– Nếu bạn là một API developer, bạn có thể sử dụng các metaclasses.

Và có một trích dẫn của ông Tim Peters như sau:

“Metaclasses là loại ma thuật sâu sắc mà 99% người dùng không bao giờ nên lo lắng về chúng. Nếu bạn phân vân liệu rằng mình có cần tới chúng hay không, điều đó có nghĩa là bạn không cần tới chúng đâu (người mà thật sự cần tới chúng sẽ biết chắc chắn rằng họ cần chúng, mà không cần phải tìm kiểm câu trả lời cho lý do vì sao”.

Nguồn và Tài liệu tiếng anh tham khảo:

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!