Bỏ qua để đến nội dung

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, type là 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 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 list
Animal = Dog
print(Animal().speak()) # gâu
Dog.legs = 4 # thêm thuộc tính cho class lúc chạy
print(Dog().legs) # 4

Dog 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.

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âu
print(Dog.__name__) # Dog

Kế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"])
print(User(id=1, email="[email protected]")) # User(id=1, email='[email protected]')

Đ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 Child

Hai đ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 trong type.__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 obj

Ghi đè Meta.__call__ cho phép kiểm soát việc tạo instance của mọi class dùng metaclass đó.

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 Service
Meta.__init__: khởi tạo class Service
--- class đã được tạo ---
Meta.__call__: tạo instance của Service
Service.__init__

Quy ước đặt tên: trong metaclass, tham số đầu của __new__mcls (metaclass), của các method khác là cls (class đang được tạo).

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ên
except 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.

class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
try:
class C(A, B): pass
except 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.

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,3

Chỉ 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__.

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_name
except TypeError as e:
print(e) # Order phải khai báo table_name

Lỗ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____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.

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_repr
class 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__.

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 int

Thự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 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'

Hãy chọn công cụ đơn giản nhất giải quyết được vấn đề, theo thứ tự:

  1. Hàm / class bình thường - thường là đủ.
  2. Class decorator - biến đổi một class cụ thể.
  3. __init_subclass__ - tự động áp dụng cho mọi class con: đăng ký, kiểm tra, cấu hình.
  4. Descriptor + __set_name__ - logic cho từng thuộc tính.
  5. Metaclass - chỉ khi cần __prepare__, __call__, hoặc thay đổi hành vi của chính object class.
  1. Dùng __init_subclass__ viết class Command sao cho mọi class con tự đăng ký theo tên viết thường (class Deploy(Command)"deploy"), và hàm run(name, *args) gọi đúng lệnh.
  2. Viết metaclass FrozenClassMeta ngăn gán/sửa thuộc tính của class sau khi được tạo (gợi ý: __setattr__ trong metaclass).
  3. 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ằng type(...) vì slots không thêm được sau khi class đã tạo).
  • Class là object; type là 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____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.