🐍Python 函数与面向对象

元类 metaclass

难度:⭐⭐⭐ | 高频指数:🔥

面试回答

常见问法

  • 什么是元类?Python 里”一切皆对象”怎么理解?
  • typeobject 是什么关系?
  • 元类有什么实际用途?
  • __init_subclass__ 和元类有什么区别?
  • 你在项目中用过元类吗?

回答

在 Python 中,类本身也是对象,而创建类的东西就是元类。默认情况下,所有类的元类是 type

class Foo:
    pass

print(type(Foo))        # <class 'type'>
print(type(type))       # <class 'type'>  ← type 是自己的元类
print(isinstance(Foo, type))  # True

关系链:实例 → 类 → 元类,即 objFoo 的实例,Footype 的实例。

元类的核心能力:在类被创建时拦截并修改类的定义

面试建议:说清楚概念 + 一个实际用例就够。不需要深入底层,但要表达”知道它是什么、什么时候该用、什么时候不该用”。

追问

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)typeobject 的关系?

# 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__ 能覆盖大部分需求(注册、验证),更简单且无元类冲突问题。面试中的正确姿态是:了解原理、知道框架在用、自己优先选择更轻量的方案。

Related · 函数与面向对象