Python metaclass

Python metaclass
Photo by Seema Miah from Unsplash

Metaclass là một chủ đề khá ít người đề cập của Python. Một phần có lẽ bởi vì nó cũng ít được sử dụng, mà một phần lý do bởi vì Python cũng ít có tài liệu về chủ đề này. Trong bài viết này, tôi sẽ giới thiệu những hiểu biết mình đã tổng hợp được từ nhiều nguồn khác nhau, liên qua tới metaclass của Python. Có thể nó ít được sử dụng bởi nó cũng không cần thiết cho lắm, tuy nhiên, hiểu biết cặn kẽ về ngôn ngữ không bao giờ là thừa cả.

Những code trong bài viết này đề sử dụng Python 3, nên có thể nó sẽ không hoạt động đúng với Python 2.

Khái niệm cơ bản

Chúng ta sẽ dần dần tìm hiểu từng khái niệm, từ cơ bản đến nâng cao để có thể hiểu được metaclass của Python.

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

Trước khi hiểu được metaclass, chúng ta cần hiểu được class của Python trước. Python có những ý tưởng rất bá đạo về việc xây dựng class, lấy cảm hứng từ ngôn ngữ Smalltalk.

Trong phần lớn ngôn ngữ lập trình, class là phần code miêu tả cách tạo ra các đối tượng. Python cũng tương tự như vậy.

>>> class Foo(object):
...     pass
...
>>> bar = Foo()
>>> print(bar)
<__main__.Foo object at 0x7f9eba142ba8>

Nhưng không chỉ có vậy, trong Python, class cũng là đối tượng. Ngay khi bạn sử dụng từ khóa class, Python sẽ thực thi nó là tạo ra một đối tượng.

Ví dụ, khi bạn khai báo class

>>> class Foo(object):
...     pass
...

Thì trong bộ nhớ, một đối tượng tên là Foo sẽ được tạo ra. Tuy nhiên, đây là một đối tượng đặc biệt, đối tượng dùng để tạo ra các đối tượng khác (instance của class). Tuy nhiên, vì là một đối tượng, nên bạn có thể:

  • gán chúng cho các biến
  • copy nó
  • thêm thuộc tính cho nó
  • dùng nó làm tham số cho các hàm

Ví dụ:

>>> print(Foo) # bạn có thể print vì class cũng là đối tượng
<class '__main__.Foo'>
>>> def echo(o):
...     print(o)
...
>>> echo(Foo) # bạn có thể truyền class làm tham số cho hàm
<class '__main__.Foo'>
>>> print(hasattr(Foo, 'attribute'))
False
>>> Foo.attribute = 'foo' # bạn có thể thêm thuộc tính cho class
>>> print(hasattr(Foo, 'attribute'))
True
>>> print(Foo.attribute)
foo
>>> FooCopy = Foo # bạn có thể gán class cho biến
>>> print(FooCopy.attribute)
foo
>>> print(FooCopy())
<__main__.Foo object at 0x7f9eba142da0>

Tạo ra các class động

Bởi vì class cũng là đối tượng, nên chúng ta có thể tạo ra các class động, giống như tất cả các đối tượng khác.

Trước hết, bạn có thể tạo ra class trong một hàm như sau:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return class chứ không phải instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # in ra class
<class '__main__.choose_class.<locals>.Foo'>
>>> print(MyClass()) # tạo ra một instance của class
<__main__.choose_class.<locals>.Foo object at 0x7f9eba142eb8>

Tuy nhiên, như vậy cũng chưa phải là động lắm, bởi vì bạn vẫn cần phải code toàn bộ nội dung của class. Bởi vì class cũng là đối tượng, nó nhất định được tạo ra bởi một thứ gì khác.

Khi bạn dùng từ khóa class, Python tự động tạo đối tượng đó cho bạn. Tuy nhiên, bạn vẫn có thể làm việc này bằng tay.

Python có hàm type, cho chúng ta biết loại của đối tượng được truyền vào.

>>> type(1)
<class 'int'>
>>> type("1")
<class 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<class 'type'>

Như vậy, hàm type có thể cho chúng ta biến class của đối tượng được truyền vào. Nó cũng tương tự như hàm isinstance vậy. Và bạn có để ý kết quả của ví dụ cuối cùng ở trên không? Class Foo là instance của class type.

>>> isinstance(bar, Foo)
True
>>> isinstance(Foo, type)
True

Vậy là class cũng là đối tượng, và đối tượng này được tạo bởi một thứ khác. Chúng ta gọi những thứ tạo ra class đó là là metaclass. Và như ví dụ trên, type là một metaclass như thế. Chúng ta có thể khái quát mối quan hệ đó bằng sơ đồ dưới đây.

relations

Mọi thứ trong Python, bất kể số, xâu, hàm, phương thức, v.v đều là đối tượng trong Python. Tất cả chúng đều là đối tượng, nên chúng phải được tạo ra bởi một class. Và chúng đều có thuộc tính __class__ để chúng ta kiểm tra việc này.

>>> age = 25
>>> age.__class__
<class 'int'>
>>> name = "AnhTN"
>>> name.__class__
<class 'str'>
>>> def foo():
...     pass
...
>>> foo.__class__
<class 'function'>
>>> class Bar(object):
...     pass
...
>>> bar = Bar()
>>> bar.__class__
<class '__main__.Bar'>

Bây giờ, chúng ta sẽ kiểm tra __class__ của mọi class?

>>> age.__class__.__class__
<class 'type'>
>>> name.__class__.__class__
<class 'type'>
>>> foo.__class__.__class__
<class 'type'>
>>> bar.__class__.__class__
<class 'type'>

Vậy, type là một metaclass có sẵn của Python. Và mặc định, các class trong Python đều được tạo ra bởi type. Tất nhiên là bạn có thể tạo ra metaclass của riêng mình nếu muốn. Chúng ta sẽ dần dần tìm hiểu trong những phần tiếp theo.

Bây giờ, chúng ta sẽ tìm hiểu kỹ hơn về quá trình tạo ra một class.

Sử dụng metaclass cơ bản

Ở ví dụ trước, chúng ta đã thấy, type là một hàm cho chúng ta biết class được dùng để tạo ra đối tượng. Và sau đó, chúng ta lại thấy, type còn có khá năng khác nữa. Nó là một metaclass được dùng để tạo ra các class.

Một hàm và một metaclass có cùng tên nên nhiều khi nó gây hiểu nhầm. Đây là vấn đề liên quan đến tương thích ngược của Python nên vấn đề sẽ còn tồn tại, không biết đến bao giờ.

type (metaclass) sẽ nhận đầu vào là những mô tả của class và trả kết quả là một class.

type(<name of the class>,
     <tuple of the parent class (for inheritance, can be empty)>,
     <dictionary containing attributes names and values)>

Ví dụ:

>>> class Foo(object):
...     pass
...

Có thể thực hiện bằng tay như sau:

>>> Foo = type('Foo', (), {}) # kết quả trả về là một class
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo()) # tạo một instance của class `Foo`
<__main__.Foo object at 0x7f9eba142f98>

Trong ví dụ trên, chúng ta sử dụng tên Foo làm tên của class và cũng là biến nhận kết quả trả về của type. Kết quả trả về này chính là một class tên Foo. Tất nhiên là chúng ta có thể dùng một biến với tên khác, nhưng như vậy sẽ khó hiểu hơn kha khá.

type chấp nhận một dictionary định nghĩa các thuộc tính của class đó. Nên:

>>> class Foo(object):
...     bar = True
...

Có thể thực hiện bằng

>>> Foo = type('Foo', (), {'bar': True})

Và chúng ta có thể sử dụng Foo như những class bình thường khác.

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> foo = Foo()
>>> print(foo)
<__main__.Foo object at 0x7f9eba142f98>
>>> print(foo.bar)
True

Chúng ta cũng có thể sử dụng cách này với các class kế thừa nhau. Ví dụ:

>>> class FooChild(Foo):
...     pass
...

có thể thực hiện bằng:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # thuộc tính `bar` kế thừa từ `Foo`
True

Nếu bạn muốn thêm phương thức cho class của bạn. Rất đơn giản, hãy định nghĩa một hàm với tham số phù hợp (self) và gán nó như là thuộc tính của class.

>>> def echo_bar(self):
...     print(self.bar)
...
>>> FooChild = type('FooChild', (Foo, ), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> foo_child = FooChild()
>>> foo_child.echo_bar()
True

Bạn cũng có thể thêm các thuộc tính kể cả khi class đã được định nghĩa rồi, giống như thêm phương thức cho các class thông thường khác.

>>> def more_method(self):
...     print("Another method")
...
>>> FooChild.more_method = more_method
>>> hasattr(FooChild, 'more_method')
True
>>> foo_child.more_method()
Another method

Bài học rút ra là gì? Đó là class của Python cũng là đối tượng, và chúng ta có thể tạo ra chúng động, tạo ra khi cần và thêm thuộc tính nếu muốn.

Và class là một đối tượng đặc biệt, nó được tạo ra bởi metaclass. Bản thân lệnh class cũng chứa nhiều bí ẩn phía sau. Đó là thiết lập các thuộc tính __qualname__, __doc__, cũng như gọi phương thức __prepare__. Chúng ta sẽ tìm hiểu về chúng ở phần sau.

Tổng kết: Thế nào là metaclass

Class là công cụ để tạo ra các đối tượng. Bạn định nghĩa class và sau đó dùng class để tạo ra các đối tượng.

Và bây giờ, chúng ta biết rằng, class của Python cũng là đối tượng (đặc biệt). Và metaclass là công cụ để tạo ra những đối tượng đặc biệt này. Metaclass có thể gọi là class của class.

Và chúng ta cũng học cách sử dụng type để tạo ra các class. Đó là bởi vì type cũng là metaclass. Và thực sự type là metaclass của mọi class trong Python.

Có khi nào bạn thắc mắc, tại sao class của class (type) lại chỉ viết thường không? Tại sao không phải là Type?

Thực ra tôi cũng không biết, nhưng chắc nó cũng tương tự như trường hợp của các class int (số nguyên), str (xâu ký tự). type cũng là một class để tạo ra các class khác.

Kiến thức nâng cao

Quá trình tạo instance

Sơ đồ dưới đây minh hoạt quá trình tạo ra một instance:

instance creation workflow

Sơ đồ trên có thể hiểu như sau:

  • Khi tạo một instance Metaclass.__call__ sẽ gọi Class.__new__.
  • Class.__new__ sẽ trả về một instance của Class.
  • Metaclass.__call__ sẽ trả về bất cứ thứ gì mà Class.__new__ trả về. Nếu một instance của Class được trả về thì nó sẽ gọi Class.__init__.

Quá trình tạo class

Việc tạo ra class, thực ra là tạo một instance của metaclass, nên cũng tương tự như vậy:

class creation workflow

Một số lưu ý:

  • Metaclass.__prepare__ trả về một đối tượng làm namespace.
  • Metaclass.__new__ trả về đối tượng Class.
  • MetaMetaclass.__call__ trả về bất cứ thứ gì Metaclass.__new__ trả về. Nếu nó trả về một instance của Metaclass thì nó cũng gọi phương thức Metaclass.__init__.

Các phương thức __prepare__, __new__, __init__ được gọi là magic method. Và chúng ta sẽ tìm hiểu về chúng kỹ hơn ở phần sau.

Như vậy, metaclass cho phép chúng ta thay đổi hầu hết các bước trong vòng đời của một đối tượng.

Metaclass là callable

Nhìn vào sơ đồ trên, bạn sẽ để ý rằng, việc tạo ra một instance phải đi qua Metaclass.__call__. Điều đó có nghĩa là bạn có thể dùng bất cứ callable nào làm metaclass.

Callable là bất cứ thứ gì có thể gọi và thực thi được. Một đối tượng là callable nếu:

  • là một instance của class có phương thức __call__
  • một loại dữ liệu có thuộc tính tp_call (struct của C)

Phương thức __call__ được gọi khi instance được gọi như một hàm.

>>> class Foo(metaclass=print):
...     pass
..
Foo () {'__module__': '__main__', '__qualname__': 'Foo'}
>>> print(Foo)
None

Nếu bạn sử dụng hàm làm metaclass, thì các class sẽ không kế thừa thừ metaclass của hàm đó, mà nó sử dụng bất cứ thứ gì mà hàm đó trả về.

Class con kế thừa metaclass

Một lợi ích so sánh với các class decorator là class con kế thừa metaclass.

Đây là kết quả của việc Metaclass.__call__ trả kết quả là một đối tượng có __class__Metaclass.

Hạn chế dùng nhiều metaclass

Python cho phép bạn định nghĩa một class kế thừa từ nhiều class khác nhau. Python cũng cho phép chúng ta định nghĩa nhiều metaclass khác nhau. Tuy nhiên, có một yêu cầu bắt buộc, đó là mọi thứ phải tuyến tính – cây kế thừa chỉ có 1 lá duy nhất.

Trong ví dụ dưới đây, việc kế thừa không được phép vì đang tồn tại hai lá (Meta1Meta2)

>>> class Meta1(type):
...     pass
...
>>> class Meta2(type):
...     pass
...
>>> class Base1(metaclass=Meta1):
...     pass
...
>>> class Base2(metaclass=Meta2):
...     pass
...
>>> class FooBar(Base1, Base2):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Nhưng ví dụ dưới đây lại hoạt động bình thường (metaclass là lá trong cây kế thừa)

>>> class Meta(type):
...     pass
...
>>> class SubMeta(Meta):
...     pass
...
>>> class Base1(metaclass=Meta):
...     pass
...
>>> class Base2(metaclass=SubMeta):
...     pass
...
>>> class FooBar(Base1, Base2):
...     pass
...
>>> type(FooBar)
<class '__main__.SubMeta'>

Magic method

Một tính năng rất đặc trưng của Python mà magic method: nó cho phép lập trình viên có thể thay đổi hoạt động của các phép toán và cả các đối tượng. Bạn có thể làm nó như ví dụ dưới đây:

>>> class FooBar:
...     def __call__(self):
...         print("Class works like a function")
...
>>> f = FooBar()
>>> f()
Class works like a function

Metaclass hoạt động dựa vào rất nhiều magic method khác nhau, nên việc tìm hiểu chúng khá cần thiết.

Slot

Khi bạn định nghĩa một magic method trong class của bạn, hàm đó sẽ hoạt động giống như một pointer trong struct dùng để mô tả class. Hơn nữa, nó còn là một phần trong __dict__. Struct đó có các trường cho mỗi magic method, vì nhiều lý do mà các trường này được gọi là type slot, mỗi magic method sẽ có một type slot tương ứng.

Và chúng ta biết thêm một tính năng nữa. Đó là cài đặt thuộc tính thông qua thuộc tính __slots__. Ví dụ như sau:

>>> class FooBar:
...     __slots__ = "foo", "bar"
...
>>> foobar = FooBar()
>>> foobar.foo = 1
>>> foobar.bar = 2

Một class có thuộc tính __slots__ sẽ tạo ra các instance không có thuộc tính __dict__ (và do đó, sẽ dùng ít bộ nhớ hơn).

>>> foobar.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'FooBar' object has no attribute '__dict__'

Một hệ quả của việc này là instance của chúng ta không thể thêm bất cứ thuộc tính nào ngoài những thuộc tính đã được định nghĩa từ trước. Nếu cố cho thêm thuộc tính mới, bạn sẽ nhận được AttributeError:

>>> foobar.more_attr = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'FooBar' object has no attribute 'more_attr'

Tham chiếu thuộc tính của đối tượng

Những điều dưới đây có thể gây nhầm lần bởi nó hơi khác một chút so với Python 2.

Giả sử Class là một class có instance là instance. Nếu chúng ta cần gọi đến thuộc tính instance.foobar, quá trình sẽ diễn ra như sau:

  • Mặc định sẽ là gọi type slot cho Class.__getattribute__ (tp_getattro).
    • foobar có phải là một data descriptor trong Class.__dict__ hay không?
      • Nếu có, trả về kết quả của Class.__dict__['foobar'].__get__(instance, Class).
    • foobar có phải là một phần tử của instance.__dict__ hay không?
      • Nếu có, trả về kết quả instance.__dict__['foobar'].
    • foobar là một phần tử trong Class.__dict__ nhưng không phải là data descriptor?
      • Nếu đúng, trả về kết quả của Class.__dict__['foobar'].__get__(instance, klass).
  • Nếu thuộc tính vẫn không được tìm thấy, nếu có tồn tại Class.__getattr__ thì gọi Class.__getattr__('foobar').

Data descriptor là một đối tượng có cả phương thức __get__ và phương thức __set__. Tham khảo.

Sơ đồ sau sẽ minh họa cho quá trình này.

objects attribute lookup

Trong sơ đồ trên, dấu : được dùng thay cho dấu . để tránh những hiểu nhầm đáng tiếc.

Tham chiếu thuộc tính của class

Bởi vì class còn phải hỗ trợ các phương thức classmethodstaticmethod nên quá trình tham chiếu có phức tạp hơn một chút. Nếu bạn gọi phương thức Class.foobar quá trình này tương đối khác với việc tham chiếu instance.foobar.

Giả sử Class có metaclass là Metaclass, quá trình tham chiếu Class.foobar sẽ diễn ra như sau:

  • Mặc định là sẽ gọi type slot cho Metaclass.__getattribute__(tp_getattro).
    • foobar là data descriptor trong Metaclass.__dict__?
      • Nếu đúng, trả về kết quả của Metaclass.__dict__['foobar'].__get__(Class, Metaclass).
    • foobar tồn tại bên trong Class.__dict__ và có thể là bất kỳ descriptor nào?
      • Nếu đúng, trả về kết quả của Class.__dict__['foobar'].__get__(None, Class).
    • Class.__dict__ có chứa foobar?
      • Nếu đúng, trả về Class.__dict__['foobar'].
    • foobar tồn tại trong Metaclass.__dict__ nhưng không phải descriptor?
      • Nếu đúng, trả về kết quả Metaclass.__dict__['foobar'].__get__(Class, Metaclass).
    • Metaclass.__dict__ có chứa foobar?
      • Nếu đúng, trả về Metaclass.__dict__['foobar'].
  • Nếu thuộc tính vẫn không tìm thấy, nếu tồn tại Metaclass.__getattr__, thì gọi Metaclass.__getattr__('foobar').

Toàn bộ quá trình được minh họa bằng sơ đồ dưới đây.

classes attribute lookup

Cũng tương tự như trên, sơ đồ này sử dụng : thay cho .

Tham chiếu magic method

Với magic method, việc tham chiếu được thực hiện này trong class, trực tiếp trong struct với type slot.

  • Class của đối tượng có slot cho magic method hay không (được gọi object->ob_type->tp_<magicmethod> trong code C)? Nếu đúng, sử dụng type slot đó, nếu không, thao tác này không được hỗ trợ.

Theo diễn giải thông thường của ngôn ngữ C:

  • object->ob_type là class của đối tượng.
  • ob_type->tp_<magicmethod> là type slot.

Việc tham chiếu có vẻ đơn giản hơn, tuy nhiên type slot được bao bọc bởi hàm của bạn, nên các data descriptor hoạt động tốt.

>>> class FooBar:
...     @property
...     def __repr__(self):
...         def inner():
...             return "FooBar"
...         return inner
...
>>> repr(FooBar())
'FooBar'

Quá trình tham chiếu ở trên, tôi nói là mặc định. Vậy có cách nào thay đổi được quá trình tham chiếu mặc định này hay không? Câu trả lời là có và chúng ta sẽ tìm hiểu trong phần tiếp sau đây.

Phương thức __new__

Một điểm dễ gây nhầm lần giữa class và metaclass chính là phương thức __new__. Nó có một số quy ước rất đặc biệt.

Phương thức __new__constructor (tạo đối tượng, kết quả trả về của nó là đối tượng mới), trong khi phương thức __init__initializer (khởi tạo giá trị ban đầu cho đối tượng, khi phương thức __init__ được gọi, đối tượng đã tồn tại rồi).

Giả sử chúng ta có class như dưới dây.

class Foobar:
    def __new__(cls):
        return super().__new__(cls)

Nếu bạn xem lại những phần trước, có thể bạn sẽ cho rằng __new__ sẽ được tham chiếu ở metaclass. Nhưng không, nó được tham chiếu theo phương pháp tĩnh.

Khi class FooBar cần gọi magic phương thức __new__, nó sẽ được tham chiếu trong chính đối tượng (class) đó, chứ không tham chiếu đến các class ở tầng cao hơn. Điều này rất quan trọng, bởi cả class mà metaclass có thể định nghĩa phương thức này.

  • FooBar.__new__ được dùng để tạo ra các instance của FooBar.
  • type.__new__ được dùng để tạo ra class FooBar (một instance của type)

Phương thức __prepare__

Đây là phương thức của metaclass, nó sẽ được gọi trước khi code của class được thực thi, và phương thức bắt buộc phải trả về kết quả là một dict (hoặc tương tự một dict), kết quả này sau này sẽ được dùng làm namespace cho tất cả code của class. Phương thức này mới được thêm vào từ Python 3.0, bạn có thể tham khảo thêm ở PEP-3115.

Nếu phương thức __prepare__ trả về một đối tượng x và chúng ta có một class:

class Class(metaclass=Meta):
    a = 1
    b = 2
    c = 3

Thì x sẽ được thay đổi như sau:

x['a'] = 1
x['b'] = 2
x['c'] = 3

Đối tượng x này cần phải là một dict. Lưu ý rằng, x sẽ là được dùng làm một tham số của Metaclass.__new__ và nếu như nó không phải một instance của dict thì bạn nên convert nó trước khi gọi super().__new__.

Một điều thú vị là phương thức không tham chiếu đến phương thức __new__. Nó xuất hiện không hề có type slot và nó được tham chiếu thông qua tham chiếu thuộc tính của class.

Chữ ký của phương thức

Trong lập trình, nhất là lập trình hướng đối tượng, các phương thức được định danh bằng chữ ký của phương thức. Chữ ký này là duy nhất cho mỗi phương thức và thường bao gồm tên, số lượng, kiểu dữ liệu và thứ tự của các tham số.

Có nhiều điều quan trọng mà chúng ta chưa nhắc tới. Chữ ký của phương thức là một trong số chúng. Hãy xem xét các class mà metaclass với những công cụ được cài đặt sẵn.

>>> class Meta(type):
...     @classmethod
...     def __prepare__(mcs, name, bases, **kwargs):
...         print('Meta.__prepare__(mcs=%s, name=%r, bases=%s, **%s)' % (
...             mcs, name, bases, kwargs))
...         return {}

Như giải thích ở trên, phương thức __prepare__ có thể trả kết quả không phải là một instance của dict, vì vậy bạn cần cài đặt phương thức __new__ để xử lý.

...     def __new__(mcs, name, bases, attrs, **kwargs):
...         print('Meta.__new__(mcs=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
...             mcs, name, bases, ', '.join(attrs), kwargs))
...         return super().__new__(mcs, name, bases, attrs)

Thông thường, __init__ ít khi được cài đặt trong metaclass bởi vì không cần thiết – class đã được tạo ra trong bộ nhớ khi __init__ được gọi. Nó gần tương đương với việc có một class decorator với khác biệt là __init__ sẽ chạy khi tạo ra class con, trong khi class decorator không được gọi cho class con.

...     def __init__(cls, name, bases, attrs, **kwargs):
...         print('Meta.__init__(cls=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
...             cls, name, bases, ', '.join(attrs), kwargs))
...         return super().__init__(name, bases, attrs)

Phương thức __call__ được gọi khi tạo instance của Class.

...     def __call__(cls, *args, **kwargs):
...         print('Meta.__call__(cls=%s, args=%s, kwargs=%s)' % (
...             cls, args, kwargs))
...         return super().__call__(*args, **kwargs)
...

Sử dụng Meta, đánh dấu extra=1:

>>> class Class(metaclass=Meta, extra=1):
...     def __new__(cls, myarg):
...         print('Class.__new__(cls=%s, myarg=%s)' % (
...             cls, myarg))
...         return super().__new__(cls)
...     def __init__(self, myarg):
...         print('Class.__init__(self=%s, myarg=%s)' % (
...             self, myarg))
...         self.myarg = myarg
...         return super().__init__()
...     def __str__(self):
...         return "<instance of Class; myargs=%s>" % (
...             getattr(self, 'myarg', 'MISSING'),)
...
Meta.__prepare__(mcs=<class '__main__.Meta'>, name='Class', bases=(), **{'extra': 1})
Meta.__new__(mcs=<class '__main__.Meta'>, name='Class', bases=(), attrs=[__str__, __module__, __qualname__, __new__, __init__], **{'extra': 1})
Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(), attrs=[__str__, __module__, __qualname__, __new__, __init__], **{'extra': 1})

Lưu ý Meta.__call__ được gọi khi tạo instance của Class:

>>> Class(1)
Meta.__call__(cls=<class '__main__.Class'>, args=(1,), kwargs={})
Class.__new__(cls=<class '__main__.Class'>, myarg=1)
Class.__init__(self=<instance of Class; myargs=MISSING>, myarg=1)
<__main__.Class object at 0x7fd71e85a198>

Tại sao chúng ta nên dùng metaclass?

Sau tất cả những kiến thứ trên, câu hỏi lớn bây giờ là: Tại sao chúng ta lại cần dùng metaclass?

Thực ra, rất ít khi chúng ta dùng đến chúng:

Metaclasses are deeper magic that 99% of users should never worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that the need them, and don’t need an explanation about why).

– Python Guru Tim Peters –

Mục đích chính của metaclass là tạo ra các API. Một ví dụ điển hình cho việc này là Django ORM.

Bạn có thể định nghĩa model trong Django như bên dưới:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Nhưng nếu bạn tạo ra một đối tượng:

someone = Person(name='AnhTN', age='25')
print(someone.age)

Lệnh print ở trên không in ra một đối tượng IntegerField. Nó in ra một số nguyên, là dữ liệu được lấy ra từ cơ sở dữ liệu.

Chúng ta có thể làm việc này vì models.Model định nghĩa __metaclass__ và nó một số xử lý bên trong và kết quả là Person có thể định nghĩa rất đơn giản trong khi thực tế nó thao tác rất phức tạp với cơ sở dữ liệu.

Django làm mọi thứ trở nên đơn giản hơn bằng cách xây dựng các API và sử dụng metaclass. Mọi thao tác sẽ được thực hiện bởi API và trong suốt với người dùng.

Kết luận

Class là những đối tượng để tạo ra các instance. Đến lượt nó, class cũng là instance của metaclass.

>>> class FooBar():
...     pass
...
>>> id(FooBar)
32902216

Mọi thứ đều là đối tượng trong Python, và có thể nó là instance của một class hoặc một metaclass, ngoại trừ type.

type là metaclass của chính nó. Và nó không phải là thứ bạn có thể dễ dàng thay đổi bằng code Python. Nếu muốn làm gì, có lẽ bạn phải xem lại mã nguồn của ngôn ngữ.

Ngoài ra, metaclass rất phức tạp, và bạn không cần thiết phải sử dụng nó. Bạn có thể thay đổi hành vi của một class bằng một trong hai cách sau, đơn giản hơn nhiều:

Đây là những gì tôi đã tìm hiểu được. Nó có rất nhiều thứ và thực sự, metaclass quá phức tạp. Có thể tôi sẽ trở lại với chủ đề này trong tương lai, vì tôi cũng cảm thấy hình như còn thiếu gì đó!!

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.