class不只是一段声明,它是一台由type驱动的类工厂。看懂这台机器,你就能亲手造类。
你将学到
- 为什么说「类也是对象」,以及
type在这句话里的确切含义 - 一条
class Foo: ...背后完整的创建流程(__prepare__→ 命名空间 → 元类 →__init_subclass__) - 用元类实现单例、注册表、ORM 风格声明,以及元类 /
__init_subclass__/ 类装饰器的取舍
前置知识
读完 序章 即可。本篇是大师篇第一篇,不要求你懂元类,但要求你敢读陌生代码。
一、「类也是对象」和 type 的两副面孔
在 Python 里,「对象」意味着可以被赋值、传参、被 type() 查询类型。类满足全部三条:
class Foo: pass
print(type(Foo)) # 输出: <class 'type'>
print(isinstance(Foo, type)) # 输出: True
print(type(Foo())) # 输出: <class '__main__.Foo'>
type(Foo()) 是 Foo(实例的类型是类),而 type(Foo) 是 type(类本身是 type 的实例)。「类的类」就是元类(metaclass)。绝大多数类的元类都是 type,int、str、list 也不例外。
type 有两种完全不同的用法:
print(type(42)) # 用法 1:查类型 -> <class 'int'>
# 用法 2:造类,type(类名, 基类元组, 命名空间字典)
Point = type("Point", (), {
"dim": 2,
"describe": lambda self: f"一个 {self.dim}D 点",
})
print(Point().describe()) # 输出: 一个 2D 点
这就是 class 语句的真相:class Point: ... 执行后,等价于调用 type(...) 造出一个类对象,再绑定到名字 Point。
二、一条 class 语句的完整流程
自定义元类可以插入流程的每一步:
class Meta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwds): # 步骤 1:准备命名空间
print(f"[1] __prepare__ 为 {name} 准备命名空间")
return {} # 必须是 mapping
def __new__(mcs, name, bases, ns, **kwds): # 步骤 2:创建类对象
public = [k for k in ns if not k.startswith("__")]
print(f"[2] __new__ 收集到的类体名字: {public}")
return super().__new__(mcs, name, bases, ns)
def __init__(cls, name, bases, ns, **kwds): # 步骤 3:初始化类对象
print(f"[3] __init__ 类 {name} 就绪")
super().__init__(name, bases, ns)
class Base(metaclass=Meta):
def __init_subclass__(cls, **kwds): # 步骤 4:子类创建时触发
print(f"[4] __init_subclass__ 被 {cls.__name__} 触发")
super().__init_subclass__(**kwds)
print(">>> 开始定义 Child")
class Child(Base):
x = 1
print(">>> 定义结束")
# 输出:
# [1] __prepare__ 为 Base 准备命名空间
# [2] __new__ 收集到的类体名字: []
# >>> 开始定义 Child
# [1] __prepare__ 为 Child 准备命名空间
# [2] __new__ 收集到的类体名字: ['x']
# [4] __init_subclass__ 被 Child 触发
# [3] __init__ 类 Child 就绪
# >>> 定义结束
顺序要点:
- 先找元类:就近取「
metaclass=参数 > 基类的元类 >type」,多个基类元类冲突会报TypeError。 __prepare__先于类体执行,返回一个 mapping(默认就是dict);类体里的x = 1、def m(...)都写进它。- 元类被调用(
Meta(name, bases, ns)):__new__在super().__new__内部一次性完成「造类 + 对描述符调用__set_name__+ 触发父类__init_subclass__」(见 02 - 描述符协议),之后才轮到__init__。 - 类对象绑定到名字
Child。
留意输出里 [4] 出现在 [3] 之前——__init_subclass__ 在 super().__new__ 内部触发,早于元类的 __init__。顺序记不清时,跑一遍就有答案。__init_subclass__ 自动被当作 classmethod,定义时不用写 self。
三、__prepare__ 的独门用途
元类唯一「非它不可」的能力,是接管命名空间。下面让类体里的名字自动大写:
class UpperDict(dict):
def __setitem__(self, key, value):
if not (key.startswith("__") and key.endswith("__")):
key = key.upper() # 类体里的普通名字自动大写
super().__setitem__(key, value)
class UpperMeta(type):
@classmethod
def __prepare__(mcs, name, bases, **kwds):
return UpperDict()
class Conf(metaclass=UpperMeta):
timeout = 30
retries = 3
print(Conf.TIMEOUT, Conf.RETRIES) # 输出: 30 3
print("timeout" in Conf.__dict__) # 输出: False
命名空间是可插拔的——这一点 __init_subclass__ 和类装饰器都做不到。类头还能传关键字参数,一路透传到 __prepare__ / __new__,第四节就会用到。
四、实战:单例元类
元类拦截实例化(Foo()),靠的是元类的 __call__:
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# 这个方法拦截的是「Foo(...)」这个实例化动作
if cls not in cls._instances:
with cls._lock: # 双检锁,保证线程安全
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Config(metaclass=SingletonMeta):
def __init__(self, path="/etc/app.conf"):
print(f"真正构造了一次: {path}")
self.path = path
a = Config()
b = Config("/tmp/other.conf") # 第二次构造被跳过
print(a is b, b.path) # 输出: True /etc/app.conf
上面 真正构造了一次: /etc/app.conf 只会打印一次。关键点:__init__ 只在第一次真正创建时执行,所以 b.path 仍是第一次的值。
五、实战:注册表——元类 vs __init_subclass__
插件注册是最常见的元类需求。先用元类实现(顺便用上类头关键字参数):
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, ns, **kwds):
cls = super().__new__(mcs, name, bases, ns)
if not kwds.get("abstract", False): # 抽象基类不注册
PluginMeta.registry[name] = cls
return cls
class Plugin(metaclass=PluginMeta, abstract=True):
pass
class CSVParser(Plugin): pass
class JSONParser(Plugin): pass
print(PluginMeta.registry)
# 输出: {'CSVParser': <class '__main__.CSVParser'>, 'JSONParser': <class '__main__.JSONParser'>}
同样的效果,用 __init_subclass__ 更短、更好读,也不需要引入元类:
class Plugin:
registry = {}
def __init_subclass__(cls, **kwds):
super().__init_subclass__(**kwds)
Plugin.registry[cls.__name__] = cls
class CSVParser(Plugin): ...
class JSONParser(Plugin): ...
print(Plugin.registry) # 输出与上面完全一致
结论先行:只是「子类创建后做点什么」(注册、加校验、打标记),__init_subclass__ 就够,别上元类。
六、实战:ORM 风格声明(元类 + 描述符)
真正的元类场景是在类创建时收集字段信息,这需要读到完整命名空间:
class Field:
def __init__(self, column=None):
self.column = column
def __set_name__(self, owner, name): # 类创建完成后自动被调用
self.name = name
if self.column is None:
self.column = name
class ModelMeta(type):
def __new__(mcs, name, bases, ns):
cls = super().__new__(mcs, name, bases, ns)
fields = {}
for klass in reversed(cls.__mro__): # reversed 保证子类覆盖父类
for key, value in vars(klass).items():
if isinstance(value, Field):
fields[key] = value
cls._fields = fields
return cls
class Model(metaclass=ModelMeta):
def save(self):
cols = ", ".join(f.column for f in self._fields.values())
return f"INSERT INTO {type(self).__name__.lower()} ({cols}) VALUES (...)"
class User(Model):
id = Field() # 列名自动取属性名 id
name = Field(column="user_name") # 显式指定列名
print(list(User._fields)) # 输出: ['id', 'name']
print(User._fields["name"].column) # 输出: user_name
print(User().save())
# 输出: INSERT INTO user (id, user_name) VALUES (...)
_fields 是类级别的元数据——ORM、序列化框架(Pydantic、DRF Serializer)、表单库都靠这一招在类定义时把结构摸清楚,而不是运行时靠反射硬猜。
七、三者取舍:别拿大炮打蚊子
| 需求 | 首选方案 |
|---|---|
| 子类创建后做点事(注册、校验、打标记) | __init_subclass__ |
| 给类追加/替换方法,不需要读类体语义 | 类装饰器 |
| 需要修改命名空间(重复键、注入名字、动态运算) | 元类 __prepare__ |
需要控制类对象本身(拦截实例化、改 __call__/__getattr__) |
元类 |
能用
__init_subclass__就别用类装饰器,能用类装饰器就别用元类。
元类有两个真实成本:元类冲突(多个基类元类不一致直接 TypeError),以及心智负担——读代码的人必须知道元类是什么。只有 __prepare__ 和「控制类对象」这两类需求,才值得付这个成本。
常见坑
坑 1:元类冲突。 多继承时各基类元类不一致会直接崩:
# ❌ 两个基类的元类互不相干
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
class C(A, B): pass # TypeError: metaclass conflict
# ✅ 让元类形成继承关系,取最「派生」的那个
class MetaB2(MetaA): pass # MetaB2 继承 MetaA
class B2(metaclass=MetaB2): pass
class C2(A, B2): pass
print(type(C2)) # 输出: <class '__main__.MetaB2'>
坑 2:__new__ 忘了把类对象 return 出去。
# ❌ 忘了 return,类名会绑定成 None(不报错,更坑)
class Meta(type):
def __new__(mcs, name, bases, ns):
pass
class A(metaclass=Meta): pass
print(A) # 输出: None
A() # TypeError: 'NoneType' object is not callable
# ✅ 校验归校验,最后一定把类对象交出去
class Meta2(type):
def __new__(mcs, name, bases, ns):
return super().__new__(mcs, name, bases, ns)
class A2(metaclass=Meta2): pass
print(A2) # 输出: <class '__main__.A2'>
坑 3:把普通方法写进元类,误以为是实例方法。
# ❌ Meta 上定义的 hello,接收的是「类」,不是实例
class Meta(type):
def hello(cls): return f"hi {cls.__name__}"
class Foo(metaclass=Meta): pass
print(Foo.hello()) # 输出: hi Foo —— 类上能调用
print(Foo().hello()) # AttributeError: 'Foo' object has no attribute 'hello'
# ✅ 元类方法服务于「类」,实例方法写进类体
class Bar(metaclass=Meta):
def hello(self): return "hi instance"
print(Bar().hello()) # 输出: hi instance
小结
- 类也是对象:类的「类」就是元类,默认是
type。 - 一条
class语句 = 找元类 →__prepare__→ 执行类体填命名空间 → 元类__new__(内含__set_name__与父类__init_subclass__)→ 元类__init__。 type(name, bases, ns)是手工造类的入口;元类的独有能力是__prepare__(改命名空间)和拦截实例化(__call__)。- 取舍顺序:
__init_subclass__> 类装饰器 > 元类。注册表这类需求优先用__init_subclass__,别为小事引入元类冲突的风险。
延伸阅读
- Python 官方文档 Data Model:Metaclasses
- 《Fluent Python》第 23 章,以及 Django Model、Pydantic 的字段收集源码
文章回复
0 条公开回复