元类 metaclass
难度:⭐⭐⭐ | 高频指数:🔥
面试回答
常见问法
- 什么是元类?Python 里”一切皆对象”怎么理解?
type和object是什么关系?- 元类有什么实际用途?
__init_subclass__和元类有什么区别?- 你在项目中用过元类吗?
回答
在 Python 中,类本身也是对象,而创建类的东西就是元类。默认情况下,所有类的元类是 type。
class Foo:
pass
print(type(Foo)) # <class 'type'>
print(type(type)) # <class 'type'> ← type 是自己的元类
print(isinstance(Foo, type)) # True
关系链:实例 → 类 → 元类,即 obj 是 Foo 的实例,Foo 是 type 的实例。
元类的核心能力:在类被创建时拦截并修改类的定义。
面试建议:说清楚概念 + 一个实际用例就够。不需要深入底层,但要表达”知道它是什么、什么时候该用、什么时候不该用”。
追问
1)type() 动态创建类?
# 常规写法
class Dog:
sound = "woof"
def speak(self):
return self.sound
# 等价的 type() 动态创建
Dog = type('Dog', (object,), {
'sound': 'woof',
'speak': lambda self: self.sound
})
d = Dog()
print(d.speak()) # woof
type(name, bases, namespace) 三个参数:类名、基类元组、类属性字典。
2)自定义元类怎么写?
class ValidateMeta(type):
def __new__(mcs, name, bases, namespace):
# 在类创建时检查:必须有 docstring
if name != 'Base' and not namespace.get('__doc__'):
raise TypeError(f"{name} 必须有文档字符串")
return super().__new__(mcs, name, bases, namespace)
class Base(metaclass=ValidateMeta):
"""基类"""
pass
class Good(Base):
"""这个类有文档"""
pass
# class Bad(Base): # TypeError: Bad 必须有文档字符串
# pass
元类的 __new__ 在类定义时(不是实例化时)被调用。
3)元类的实际用途有哪些?
用途一:自动注册(Registry 模式)
class PluginMeta(type):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if bases: # 跳过基类本身
PluginMeta.registry[name] = cls
return cls
class Plugin(metaclass=PluginMeta):
pass
class AuthPlugin(Plugin):
pass
class CachePlugin(Plugin):
pass
print(PluginMeta.registry)
# {'AuthPlugin': <class 'AuthPlugin'>, 'CachePlugin': <class 'CachePlugin'>}
用途二:ORM 字段收集(Django Model 风格)
元类在类创建时自动收集所有 Field 实例,生成 _fields 映射。Django Model、SQLAlchemy 的 declarative_base 都是这个原理。
用途三:接口检查
元类可以在类创建时检查子类是否实现了必要方法,类似抽象基类但更灵活。
4)__init_subclass__ 作为轻量替代
Python 3.6+ 引入 __init_subclass__,很多场景不再需要元类:
class Plugin:
_registry = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
Plugin._registry[plugin_name] = cls
class Auth(Plugin, plugin_name="auth"):
pass
class Cache(Plugin, plugin_name="cache"):
pass
print(Plugin._registry)
# {'auth': <class 'Auth'>, 'cache': <class 'Cache'>}
对比:
| 维度 | 元类 | __init_subclass__ |
|---|---|---|
| 复杂度 | 高 | 低 |
| 能力 | 可修改类创建的所有方面 | 只能在子类创建后做处理 |
| 冲突 | 多元类继承容易冲突 | 无冲突问题 |
| 适用场景 | ORM、框架级别 | 注册、验证、简单钩子 |
面试结论:优先用 __init_subclass__,只有它搞不定时才上元类。
5)type 和 object 的关系?
# type 是所有类的元类
print(type(object)) # <class 'type'>
print(type(type)) # <class 'type'>
# object 是所有类的基类
print(issubclass(type, object)) # True
# 鸡生蛋问题:type 是 object 的实例,object 是 type 的基类
# 这是解释器启动时特殊处理的,不是普通 Python 代码能做到的
面试里说一句”这是解释器层面的 bootstrap,type 和 object 互相依赖”就够了。
6)__new__ vs __init__ 在元类中?
__new__ 能决定是否创建类、修改类的 namespace;__init__ 只能对已创建的类做补充设置。元类的 __new__ 在类定义时被调用。
原理展开
1. 类创建的完整流程
class Foo(Base, metaclass=Meta):
x = 1
def method(self):
pass
等价于:
# 1. 确定元类(metaclass 参数 > 基类的元类 > type)
# 2. 准备命名空间
namespace = Meta.__prepare__('Foo', (Base,))
# 3. 执行类体(在 namespace 中执行)
exec(class_body, namespace)
# 4. 调用元类创建类对象
Foo = Meta('Foo', (Base,), namespace)
# 即 Meta.__new__(Meta, 'Foo', (Base,), namespace)
# 然后 Meta.__init__(Foo, 'Foo', (Base,), namespace)
2. 元类冲突问题
多继承时如果基类有不同元类,会报 TypeError: metaclass conflict。解决方法是创建兼容的元类(继承所有冲突元类)。这也是为什么能不用元类就不用的原因之一。
3. 实际框架中的元类
Django Model:
# Django 的 Model 使用 ModelBase 元类
# 自动收集 Field 定义、生成数据库表映射、注册到 app registry
class User(models.Model): # ModelBase 元类在背后工作
name = models.CharField(max_length=100)
email = models.EmailField()
ABC(抽象基类):
from abc import ABC, abstractmethod
class Shape(ABC): # ABCMeta 元类
@abstractmethod
def area(self):
pass
# s = Shape() # TypeError: Can't instantiate abstract class
易错点
-
滥用元类 大多数场景用
__init_subclass__、装饰器、描述符就够了。元类是最后手段。 -
元类继承冲突 多继承时如果基类有不同元类,必须手动创建兼容元类。
-
在元类
__new__里忘记调super().__new__不调用就不会创建类对象。 -
混淆元类的
__init__和普通类的__init__元类的__init__接收的是类对象(cls),不是实例。 -
以为
__init_subclass__能完全替代元类 它无法修改类创建过程本身(如__prepare__),也无法阻止类的创建。 -
把
type(obj)和type(name, bases, ns)混淆 一个参数时是查看类型,三个参数时是创建类。
记忆技巧
-
元类是”类的模具”:普通类是对象的模具,元类是类的模具。
-
层级口诀:实例 → 类 → 元类 → type(到顶了)。
-
type 身兼两职:
type(x)查类型,type(n, b, d)造类。 -
选择口诀:能用装饰器就用装饰器,能用
__init_subclass__就用它,实在不行才上元类。 -
Django 记忆法:你写
class User(Model)时,元类在背后帮你收集字段、建表映射——这就是元类最经典的用途。
面试速答版
元类是”创建类的类”。Python 中类本身也是对象,默认由 type 创建。自定义元类通过继承 type 并重写 __new__,可以在类定义时拦截和修改类的创建过程。实际用途包括:ORM 字段收集(Django Model)、插件自动注册、接口强制检查等。但日常开发中,Python 3.6+ 的 __init_subclass__ 能覆盖大部分需求(注册、验证),更简单且无元类冲突问题。面试中的正确姿态是:了解原理、知道框架在用、自己优先选择更轻量的方案。