Python 元类(中):拦截类的创建

上篇我们知道了:type 是一个类工厂,所有类都是它创建的。class 语句本质上就是一次 type() 调用。

那如果我们自己写一个类工厂,替掉 type,会怎样?

第一个元类

元类就是一个继承了 type 的类。你重写它的 __new__ 方法,就能在类创建的时候插一脚:

python 复制代码
class SimpleMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"  正在创建类: {name}")
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

class MyClass(metaclass=SimpleMeta):
    pass

# 输出: 正在创建类: MyClass
print(type(MyClass))  # <class '__main__.SimpleMeta'> ------ 不再是 type

就这么简单。metaclass=SimpleMeta 告诉 Python:「创建 MyClass 的时候,别用默认的 type(),用我的 SimpleMeta()」。

你可以多创建几个类,看看效果:

python 复制代码
class Another(metaclass=SimpleMeta):
    pass

class AndAnother(metaclass=SimpleMeta):
    pass

# 输出:
#   正在创建类: Another
#   正在创建类: AndAnother

每一个用 SimpleMeta 创建的类,都会经过 __new__。这就是元类的核心------拦截类的创建过程

参数是什么意思?

__new__ 里的四个参数,和 type() 的三个参数一一对应:

参数 含义 来源
mcs 元类本身 Python 自动传入
name 正在创建的类名 对应 type() 的第一个参数
bases 父类元组 对应 type() 的第二个参数
namespace 类体中定义的所有属性和方法的字典 对应 type() 的第三个参数

你可以把 namespace 打印出来看看:

python 复制代码
class DebugMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"  namespace = {namespace}")
        return super().__new__(mcs, name, bases, namespace)

class Sample(metaclass=DebugMeta):
    x = 10
    def hello(self):
        return "hi"

# 输出:
# namespace = {'__module__': '__main__', '__qualname__': 'Sample', 'x': 10, 'hello': <function Sample.hello at 0x...>}

x = 10def hello 都在字典里。你可以读它、改它、甚至拒绝创建这个类。

实战一:自动注入属性

理解了参数,我们来做点实用的事情。假设你有一批类都需要一个 version 属性,但你不想每个类都手动写:

python 复制代码
class AutoAttrMeta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if not hasattr(cls, 'version'):
            cls.version = "1.0"
        return cls

class Service(metaclass=AutoAttrMeta):
    pass

class AdvancedService(Service):
    version = "2.0"  # 自己定义了,不会被覆盖

class BasicService(metaclass=AutoAttrMeta):
    pass

print(Service.version)         # 1.0  ← 自动加上了
print(AdvancedService.version) # 2.0  ← 自己定义的,没被覆盖
print(BasicService.version)    # 1.0  ← 也自动加上了

逻辑很简单:类创建完之后,检查一下有没有 version,没有就补上。

这就是元类最典型的用法------在类创建的那一刻,自动做一些事情 。Django 的 Model 类就是这么干的:你写 class User(models.Model): 的时候,元类在背后自动帮你加了 __tablename____fields__ 一堆东西,你根本不用操心。

实战二:在类定义时就拦住错误

元类还能做校验。与其让代码跑到一半才发现字段名写错了,不如在类定义的时候就报错:

python 复制代码
class LowercaseMeta(type):
    def __new__(mcs, name, bases, namespace):
        for attr_name in namespace:
            if not attr_name.startswith('_') and attr_name != attr_name.lower():
                raise TypeError(
                    f"类 '{name}' 中的属性 '{attr_name}' 必须全小写!"
                )
        return super().__new__(mcs, name, bases, namespace)

class GoodClass(metaclass=LowercaseMeta):
    my_field = 1       # ✓ 小写
    another_field = 2  # ✓ 小写

print(GoodClass.my_field)  # 1 ------ 没问题

try:
    class BadClass(metaclass=LowercaseMeta):
        BadField = 1   # ✗ 大写了!
except TypeError as e:
    print(e)  # 类 'BadClass' 中的属性 'BadField' 必须全小写!

BadField 在类定义的那一刻就被拦下来了。这个错误不会等到你调用某个方法时才冒出来------它在模块被 import 的时候就会发生,非常早。

实战三:单例模式

单例的意思是:一个类只能有一个实例。不管你调用多少次 Database(),拿到的都是同一个对象。

要实现这个,我们需要拦截的不是类的创建,而是实例的创建

上篇我们讲过,当你写 Foo() 的时候,Python 实际上是在调用 type.__call__()。这个 __call__ 方法会依次调用类的 __new____init__。元类可以重写这个 __call__,在实例化时插入自己的逻辑:

python 复制代码
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            print(f"  创建 {cls.__name__} 的唯一实例")
            cls._instances[cls] = super().__call__(*args, **kwargs)
        else:
            print(f"  返回已有实例")
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    def __init__(self):
        self.connection_id = id(self)

db1 = Database()  # 创建 Database 的唯一实例
db2 = Database()  # 返回已有实例

print(db1 is db2)                        # True ------ 同一个对象
print(db1.connection_id == db2.connection_id)  # True

调用链是这样的:

复制代码
Database()
    │
    ▼
SingletonMeta.__call__(Database)   ← 元类拦截
    │
    ├─ 第一次 → super().__call__() → Database.__new__() → Database.__init__()
    │
    └─ 之后 → 直接返回缓存的实例

注意:元类的 __call__ 拦截的是 类() 这个调用,而类的 __new____init__ 负责实际创建和初始化实例。元类站在它们「上面」,决定要不要调用它们。

实战四:插件自动注册

最后一个实战例子------插件自动注册。你定义一个子类,它就自动出现在注册表里,不需要手动调用任何注册函数:

python 复制代码
class PluginMeta(type):
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "Base":          # 跳过基类本身
            mcs.registry[name] = cls
        return cls

class Base(metaclass=PluginMeta):
    pass

class AuthPlugin(Base):
    def authenticate(self):
        return "authenticated"

class LogPlugin(Base):
    def log(self, msg):
        return f"LOG: {msg}"

# 不需要手动注册,定义类的时候就完成了
print(list(PluginMeta.registry.keys()))  # ['AuthPlugin', 'LogPlugin']

# 可以通过名字动态实例化
plugin = PluginMeta.registry["AuthPlugin"]()
print(plugin.authenticate())  # authenticated

这种模式在实际项目中非常常见:插件系统、策略模式、序列化框架按类型名找处理器......本质上都是「定义即注册」。

一个关键的细节:执行时机

元类的代码,在什么时候跑?

在类定义的时候,不是实例化的时候。

python 复制代码
class TraceMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"  [元类] 正在创建 {name}")
        return super().__new__(mcs, name, bases, namespace)

print("开始定义类...")
class A(metaclass=TraceMeta):
    pass
print("类定义完成")

# 输出:
# 开始定义类...
#   [元类] 正在创建 A
# 类定义完成

模块被 import 的那一刻,元类就已经跑完了。之后你 A() 实例化的时候,元类的 __new__ 不会再执行(但 __call__ 会,如果你重写了的话)。

回顾一下

到这里你已经知道了:

  1. 元类就是一个继承 type 的类工厂 ,重写 __new__ 可以在类创建时做任何事情
  2. 重写 __call__ 可以拦截实例化,控制 __new____init__ 是否被调用
  3. 元类代码在类定义时就执行,不是实例化时

但元类的能力不止于此。它还能控制类的命名空间、自定义 isinstance 的行为、处理继承链上的元类冲突......

下一篇,我们讲这些进阶内容,以及一个更重要的问题:什么时候不该用元类。

相关推荐
我能坚持多久14 小时前
STL详解——priority_queue的使用以及模拟实现
开发语言·c++·priority_queue
Tony Bai14 小时前
从 Go 迁移到 Rust
开发语言·后端·golang·rust
牧鸯人14 小时前
基于yolov8的课堂行为检测系统——主要功能检测睡觉、手机、人数
python·深度学习·yolo·学生行为统计
江屿风14 小时前
【C++笔记】string类流食般投喂
开发语言·c++·笔记
我是一颗柠檬15 小时前
【JDK8新特性】JDK8实战与面试高频考点汇总Day12
java·开发语言·后端·面试·职场和发展
EntyIU15 小时前
langchain短期 + 长期记忆架构
python·ai
wjs202415 小时前
C# 索引器(Indexer)
开发语言
千寻girling15 小时前
机器学习 | 监督学习算法(了解) | 尚硅谷学习
开发语言·人工智能·后端·python·学习·算法·机器学习
阿方.91815 小时前
C++ string 超全精讲 | 从零使用、底层原理、手搓简易string、高频考点、易错点、面试手撕
开发语言·c++·字符串·string·知识分享