Metaclass, __init_subclass__ và quá trình tạo class trong Python
“Metaclass là phép thuật sâu hơn mức 99% người dùng cần quan tâm. Nếu bạn còn phân vân có cần chúng không, thì bạn không cần.” - Tim Peters
Câu nói trên đúng khi bạn viết code ứng dụng. Nhưng khi đọc code framework - Django ORM, SQLAlchemy, Pydantic, enum, abc - bạn sẽ gặp metaclass ở khắp nơi. Hiểu chúng giúp bạn biết các framework đó “làm phép” như thế nào, và quan trọng hơn: biết khi nào có công cụ đơn giản hơn để đạt cùng mục đích.
Trong bài này, bạn sẽ học:
- Class là object,
typelà class của mọi class - Tạo class động bằng
type(name, bases, namespace) - Từng bước Python thực hiện khi gặp câu lệnh
class - Viết metaclass với
__new__,__init__,__call__,__prepare__ __init_subclass__- cách thay thế metaclass đơn giản (3.6+)- Class decorator và
__class_getitem__ - Khi nào dùng công cụ nào
Class cũng là object
Phần tiêu đề “Class cũng là object”class Dog: sound = "gâu"
def speak(self): return self.sound
print(type(Dog)) # <class 'type'>print(isinstance(Dog, object)) # True
# Class có thể được gán vào biến, truyền vào hàm, lưu trong listAnimal = Dogprint(Animal().speak()) # gâu
Dog.legs = 4 # thêm thuộc tính cho class lúc chạyprint(Dog().legs) # 4Dog là một object, và object đó là instance của type. Tương tự như Dog() tạo ra một chú chó, type(...) tạo ra một class:
dog_instance ──instance of──► Dog ──instance of──► type ──instance of──► type (chính nó)Class tạo ra class được gọi là metaclass. type là metaclass mặc định.
Tạo class không cần câu lệnh class
Phần tiêu đề “Tạo class không cần câu lệnh class”def speak(self): return self.sound
Dog = type( "Dog", # tên (object,), # tuple các class cha {"sound": "gâu", "speak": speak}, # namespace: thuộc tính và method)
print(Dog().speak()) # gâuprint(Dog.__name__) # DogKết quả hoàn toàn giống với câu lệnh class. Kỹ thuật này hữu ích khi cần sinh class từ dữ liệu (schema JSON, bảng database):
def make_record_class(name, fields): def __init__(self, **kwargs): for f in fields: setattr(self, f, kwargs.get(f)) def __repr__(self): values = ", ".join(f"{f}={getattr(self, f)!r}" for f in fields) return f"{name}({values})" return type(name, (), {"__init__": __init__, "__repr__": __repr__, "__slots__": tuple(fields)})
User = make_record_class("User", ["id", "email"])Điều gì xảy ra khi Python gặp câu lệnh class?
Phần tiêu đề “Điều gì xảy ra khi Python gặp câu lệnh class?”class Child(Base, metaclass=Meta, flag=True): x = 1 def method(self): ...Python thực hiện các bước sau:
1. Xác định metaclass - dùng metaclass=... nếu có, nếu không thì lấy metaclass của các class cha, mặc định là type. (Chọn metaclass "dẫn xuất nhất" trong số đó.)
2. Chuẩn bị namespace namespace = Meta.__prepare__("Child", (Base,), flag=True) # mặc định: dict rỗng
3. Thực thi THÂN CLASS như một đoạn code, với namespace làm biến cục bộ -> namespace = {"x": 1, "method": <function>, "__module__": ..., "__qualname__": ...}
4. Tạo class Child = Meta("Child", (Base,), namespace, flag=True) ├─ Meta.__new__(Meta, "Child", (Base,), namespace, flag=True) -> tạo object class │ └─ bên trong type.__new__: │ - gọi __set_name__ của mọi descriptor trong namespace │ - gọi Base.__init_subclass__(Child, flag=True) └─ Meta.__init__(Child, "Child", (Base,), namespace, flag=True)
5. Áp dụng class decorator (nếu có), gán kết quả vào tên ChildHai điều đáng chú ý:
- Thân class là code chạy thật (bước 3), không chỉ là khai báo. Bạn có thể đặt
print()hay vòng lặp trong đó. __set_name__(của descriptor) và__init_subclass__đều được gọi bên trongtype.__new__- đó là hai “móc” (hook) cho phép tuỳ biến việc tạo class mà không cần metaclass.
Còn khi bạn tạo instance bằng Child():
Child(*args) == type(Child).__call__(Child, *args) # tức là Meta.__call__ ├─ obj = Child.__new__(Child, *args) └─ if isinstance(obj, Child): Child.__init__(obj, *args) └─ return objGhi đè Meta.__call__ cho phép kiểm soát việc tạo instance của mọi class dùng metaclass đó.
Viết metaclass đầu tiên
Phần tiêu đề “Viết metaclass đầu tiên”Metaclass là class kế thừa type:
class Meta(type): def __new__(mcls, name, bases, namespace, **kwargs): print(f"Meta.__new__: tạo class {name}") cls = super().__new__(mcls, name, bases, namespace, **kwargs) return cls
def __init__(cls, name, bases, namespace, **kwargs): print(f"Meta.__init__: khởi tạo class {name}") super().__init__(name, bases, namespace, **kwargs)
def __call__(cls, *args, **kwargs): print(f"Meta.__call__: tạo instance của {cls.__name__}") return super().__call__(*args, **kwargs)
class Service(metaclass=Meta): def __init__(self): print("Service.__init__")
print("--- class đã được tạo ---")Service()Meta.__new__: tạo class ServiceMeta.__init__: khởi tạo class Service--- class đã được tạo ---Meta.__call__: tạo instance của ServiceService.__init__Quy ước đặt tên: trong metaclass, tham số đầu của __new__ là mcls (metaclass), của các method khác là cls (class đang được tạo).
Ví dụ 1: Singleton bằng __call__
Phần tiêu đề “Ví dụ 1: Singleton bằng __call__”class SingletonMeta(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 Config(metaclass=SingletonMeta): def __init__(self): print("đọc file config...")
a = Config() # đọc file config...b = Config() # (không in gì)print(a is b) # True(Trong Python, một module cũng là singleton tự nhiên - thường chỉ cần tạo một object ở cấp module là đủ.)
Ví dụ 2: __prepare__ - kiểm soát namespace của class
Phần tiêu đề “Ví dụ 2: __prepare__ - kiểm soát namespace của class”__prepare__ trả về object dùng làm namespace khi chạy thân class. Ví dụ: phát hiện method bị định nghĩa trùng tên (lỗi copy-paste rất khó thấy):
class NoDuplicates(dict): def __setitem__(self, key, value): if key in self and not key.startswith("__"): raise TypeError(f"'{key}' được định nghĩa hai lần!") super().__setitem__(key, value)
class StrictMeta(type): @classmethod def __prepare__(mcls, name, bases, **kwargs): return NoDuplicates()
def __new__(mcls, name, bases, namespace, **kwargs): return super().__new__(mcls, name, bases, dict(namespace), **kwargs)
try: class Api(metaclass=StrictMeta): def get_user(self): ... def get_user(self): ... # copy-paste quên đổi tênexcept TypeError as e: print(e) # 'get_user' được định nghĩa hai lần!Thư viện enum dùng chính kỹ thuật này: namespace đặc biệt _EnumDict báo lỗi khi bạn khai báo trùng tên thành viên.
Xung đột metaclass
Phần tiêu đề “Xung đột metaclass”class MetaA(type): passclass MetaB(type): passclass A(metaclass=MetaA): passclass B(metaclass=MetaB): pass
try: class C(A, B): passexcept TypeError as e: print(e) # metaclass conflict: the metaclass of a derived class must be ...Đây là một lý do lớn để tránh metaclass trong thư viện: người dùng không thể kết hợp class của bạn với class của thư viện khác cũng dùng metaclass (ví dụ muốn một Django model đồng thời là ABC).
__init_subclass__: đủ dùng cho 90% trường hợp
Phần tiêu đề “__init_subclass__: đủ dùng cho 90% trường hợp”PEP 487 (Python 3.6) thêm một hook đơn giản: phương thức __init_subclass__ của class cha được gọi mỗi khi có class con được tạo.
Registry các plugin
Phần tiêu đề “Registry các plugin”class Exporter: registry = {}
def __init_subclass__(cls, /, format_name, **kwargs): super().__init_subclass__(**kwargs) cls.format_name = format_name Exporter.registry[format_name] = cls
def export(self, data): raise NotImplementedError
class JsonExporter(Exporter, format_name="json"): def export(self, data): import json return json.dumps(data)
class CsvExporter(Exporter, format_name="csv"): def export(self, data): return ",".join(map(str, data))
def export(data, fmt): return Exporter.registry[fmt]().export(data)
print(Exporter.registry) # {'json': <class 'JsonExporter'>, 'csv': <class 'CsvExporter'>}print(export([1, 2, 3], "csv")) # 1,2,3Chỉ cần định nghĩa class con là nó tự đăng ký - không cần danh sách thủ công. Tham số từ khoá trong câu lệnh class (format_name="json") được truyền thẳng vào __init_subclass__.
Kiểm tra class con khi định nghĩa
Phần tiêu đề “Kiểm tra class con khi định nghĩa”class Model: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if not hasattr(cls, "table_name"): raise TypeError(f"{cls.__name__} phải khai báo table_name")
class User(Model): table_name = "users" # OK
try: class Order(Model): pass # quên table_nameexcept TypeError as e: print(e) # Order phải khai báo table_nameLỗi xuất hiện ngay lúc import module, không phải khi code chạy tới chỗ dùng Order - phát hiện sớm hơn nhiều.
Metaclass so với __init_subclass__:
| Nhu cầu | __init_subclass__ |
Metaclass |
|---|---|---|
| Đăng ký / kiểm tra class con | ✅ | ✅ |
| Thêm/sửa thuộc tính class con | ✅ | ✅ |
Tuỳ biến namespace (__prepare__) |
❌ | ✅ |
Kiểm soát việc tạo instance (__call__) |
❌ (dùng __new__ của class) |
✅ |
Thay đổi hành vi của chính class (len(MyClass), for x in MyClass) |
❌ | ✅ |
| Kết hợp với class khác không lo xung đột | ✅ | ❌ |
Ví dụ hàng cuối: for color in Color với Enum chạy được vì EnumMeta (metaclass của Enum) định nghĩa __iter__ và __len__ - những phương thức áp dụng lên class chứ không phải instance. Đây là việc chỉ metaclass làm được.
Class decorator: lựa chọn đơn giản nhất
Phần tiêu đề “Class decorator: lựa chọn đơn giản nhất”Nếu bạn chỉ cần biến đổi một class sau khi nó được tạo, và không cần tự động áp dụng cho class con, class decorator là đơn giản nhất:
import inspect
def add_repr(cls): fields = list(inspect.get_annotations(cls)) # an toàn cả với annotation "lười" của 3.14 def __repr__(self): return f"{cls.__name__}(" + ", ".join(f"{f}={getattr(self, f)!r}" for f in fields) + ")" cls.__repr__ = __repr__ return cls
@add_reprclass Point: x: int y: int def __init__(self, x, y): self.x, self.y = x, y
print(Point(1, 2)) # Point(x=1, y=2)@dataclass chính là một class decorator như vậy - nó đọc __annotations__ rồi sinh __init__, __repr__, __eq__.
__class_getitem__: cú pháp MyClass[int]
Phần tiêu đề “__class_getitem__: cú pháp MyClass[int]”Muốn class của bạn hỗ trợ cú pháp generic như list[int] mà không cần metaclass:
class Box: def __class_getitem__(cls, item): return f"{cls.__name__} chứa {item.__name__}" # thường trả về types.GenericAlias
print(Box[int]) # Box chứa intThực tế bạn ít khi tự viết - kế thừa typing.Generic (hoặc dùng cú pháp class Box[T]: từ 3.12) sẽ lo việc này. Xem bài Type hints nâng cao.
Metaclass trong thư viện chuẩn
Phần tiêu đề “Metaclass trong thư viện chuẩn”| Metaclass | Dùng ở | Làm gì |
|---|---|---|
abc.ABCMeta |
ABC |
Ngăn tạo instance nếu còn @abstractmethod chưa cài đặt; register() subclass ảo |
enum.EnumMeta |
Enum |
Biến thuộc tính class thành thành viên enum, cho phép len(), for, Color["RED"] |
typing nội bộ |
Protocol, NamedTuple, TypedDict |
Sinh class từ annotation |
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self): ...
try: Shape()except TypeError as e: print(e) # Can't instantiate abstract class Shape without an implementation for abstract method 'area'Nên dùng công cụ nào?
Phần tiêu đề “Nên dùng công cụ nào?”Hãy chọn công cụ đơn giản nhất giải quyết được vấn đề, theo thứ tự:
- Hàm / class bình thường - thường là đủ.
- Class decorator - biến đổi một class cụ thể.
__init_subclass__- tự động áp dụng cho mọi class con: đăng ký, kiểm tra, cấu hình.- Descriptor +
__set_name__- logic cho từng thuộc tính. - Metaclass - chỉ khi cần
__prepare__,__call__, hoặc thay đổi hành vi của chính object class.
Bài tập
Phần tiêu đề “Bài tập”- Dùng
__init_subclass__viết classCommandsao cho mọi class con tự đăng ký theo tên viết thường (class Deploy(Command)→"deploy"), và hàmrun(name, *args)gọi đúng lệnh. - Viết metaclass
FrozenClassMetangăn gán/sửa thuộc tính của class sau khi được tạo (gợi ý:__setattr__trong metaclass). - Viết class decorator
@auto_slotsđọc__annotations__và tạo lại class với__slots__tương ứng (gợi ý: phải tạo class mới bằngtype(...)vì slots không thêm được sau khi class đã tạo).
Kết luận
Phần tiêu đề “Kết luận”- Class là object;
typelà metaclass mặc định tạo ra chúng. - Câu lệnh
class: chọn metaclass →__prepare__→ chạy thân class →Meta(name, bases, ns)→ decorator. __set_name__và__init_subclass__được gọi trong quá trình tạo class - hai hook mạnh mà không cần metaclass.- Metaclass cần thiết khi bạn phải tuỳ biến namespace, việc tạo instance, hoặc hành vi của chính class - nhưng dễ gây xung đột khi kết hợp.
- Luôn chọn công cụ đơn giản nhất: decorator →
__init_subclass__→ metaclass.
Bài tiếp theo: Hệ thống import của Python.