Python metaclass
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.
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:
Sơ đồ trên có thể hiểu như sau:
- Khi tạo một instance
Metaclass.__call__
sẽ gọiClass.__new__
. Class.__new__
sẽ trả về một instance củaClass
.Metaclass.__call__
sẽ trả về bất cứ thứ gì màClass.__new__
trả về. Nếu một instance củaClass
được trả về thì nó sẽ gọiClass.__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:
Một số lưu ý:
Metaclass.__prepare__
trả về một đối tượng làm namespace.Metaclass.__new__
trả về đối tượngClass
.MetaMetaclass.__call__
trả về bất cứ thứ gìMetaclass.__new__
trả về. Nếu nó trả về một instance củaMetaclass
thì nó cũng gọi phương thứcMetaclass.__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__
là 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á (Meta1
và Meta2
)
>>> 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 trongClass.__dict__
hay không?- Nếu có, trả về kết quả của
Class.__dict__['foobar'].__get__(instance, Class)
.
- Nếu có, trả về kết quả của
foobar
có phải là một phần tử củainstance.__dict__
hay không?- Nếu có, trả về kết quả
instance.__dict__['foobar']
.
- Nếu có, trả về kết quả
foobar
là một phần tử trongClass.__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 đúng, trả về kết quả của
- 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ọiClass.__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.
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 classmethod
và staticmethod
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 trongMetaclass.__dict__
?- Nếu đúng, trả về kết quả của
Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
.
- Nếu đúng, trả về kết quả của
foobar
tồn tại bên trongClass.__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)
.
- Nếu đúng, trả về kết quả của
Class.__dict__
có chứafoobar
?- Nếu đúng, trả về
Class.__dict__['foobar']
.
- Nếu đúng, trả về
foobar
tồn tại trongMetaclass.__dict__
nhưng không phải descriptor?- Nếu đúng, trả về kết quả
Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
.
- Nếu đúng, trả về kết quả
Metaclass.__dict__
có chứafoobar
?- Nếu đúng, trả về
Metaclass.__dict__['foobar']
.
- Nếu đúng, trả về
- Nếu thuộc tính vẫn không tìm thấy, nếu tồn tại
Metaclass.__getattr__
, thì gọiMetaclass.__getattr__('foobar')
.
Toàn bộ quá trình được minh họa bằng sơ đồ dưới đây.
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__
là 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__
là 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ủaFooBar
.type.__new__
được dùng để tạo ra classFooBar
(một instance củatype
)
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:
- monkey patching
- class decorator
Đâ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ì đó!!
Welcome
Đây là thế giới của manhhomienbienthuy (naa). 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
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.