资讯动态

python高级之元类的用法总结

发布时间:2026/10/17 4:40:10 来源:尧图企业网站定制
前言标题里的小写python原样保留正文统一写作Python。前面讲基础的文章说清了「元类怎么定义、怎么挂」本文总结的是稍微进阶一层的用法元类怎么和「抽象基类」配合、怎么换取类的命名空间、怎么自定义isinstance的语义。一个必须提前澄清的误解「鸭子类型」和「__subclasshook__」不是一回事。鸭子类型duck typing是运行期协议——只要对象有需要的方法就拿来用压根不查类型而__subclasshook__是让issubclass/isinstance这类显式类型检查也能认可某些「本来没有继承关系」的类。前者完全不看类型后者改了类型检查的判定规则。把这两个概念混在一起是进阶阶段最常见的认知错误。本文的四节分别是元类与ABCMeta的协作、__prepare__定制命名空间、虚拟子类与isinstance定制、元类__call__的高级拦截。文中 API 名称以 Python 官方文档为准本机没有解释器输出均为人工推演。一、元类 抽象基类自动注册具体实现抽象基类由abc.ABCMeta这个元类驱动。想同时享受「抽象方法约束」和「自动注册具体子类」就让自定义元类继承ABCMeta# 适用于 Python 3.0from abc import ABCMeta, abstractmethodclass PluginMeta(ABCMeta):plugins {}def __new__(mcls, name, bases, namespace):cls super().__new__(mcls, name, bases, namespace)if not getattr(cls, __abstractmethods__, None):mcls.plugins[name] cls # 没有抽象方法的才是具体实现return clsclass Plugin(metaclassPluginMeta):abstractmethoddef run(self):...class Loader(Plugin):def run(self):return loadingprint(sorted(PluginMeta.plugins)) # [Loader]关键在__abstractmethods__这是由元类设置的一个frozenset里面装着尚未实现的抽象方法名。Plugin还是抽象的包含run所以这个集合非空被跳过Loader实现了run集合为空因此被登记。注意这里必须在super().__new__之后读取它——__abstractmethods__是元类在建类过程中算出来写上去的读取时机放错就会拿到过期的值。二、__prepare__把类命名空间换成自己的映射默认的类命名空间从 Python 3.6 起就是保序映射所以换掉它通常只有一个目的加规则。下面这个元类禁止在类体里定义全大写、且不以下划线开头的名字# 适用于 Python 3.6class StrictNamespace(dict):def __setitem__(self, key, value):if key.isupper() and not key.startswith(_):raise NameError(f不允许定义全大写名字: {key})super().__setitem__(key, value)class StrictMeta(type):classmethoddef __prepare__(mcls, name, bases, **kwargs):return StrictNamespace()def __new__(mcls, name, bases, namespace, **kwargs):return super().__new__(mcls, name, bases, namespace, **kwargs)class Config(metaclassStrictMeta):timeout 30 # 小写允许# class Bad(metaclassStrictMeta):# MAX_SIZE 10 # NameError: 不允许定义全大写名字__prepare__必须是类方法返回值会被交给元类__new__的namespace参数容器里的赋值语句会触发__setitem__从而在类体执行过程中就报错。这是元类独有能力之一装饰器在类建好之后才拿到类拦不住类体本身。三、虚拟子类让 isinstance 认可「没有继承关系」的类标准库的abc给两条路。其一显式注册。用register()把某个类登记为「虚拟子类」# 适用于 Python 3.0from abc import ABCclass Sized(ABC):passSized.register(tuple)print(issubclass(tuple, Sized)) # Trueprint(isinstance((), Sized)) # Trueprint(Sized in tuple.__mro__) # False ← 不会出现在 MRO 里官方文档明确说明注册进来的虚拟子类不会出现在 MRO 中ABC 里定义的方法也不会被它调用连super()也走不到。它只影响issubclass/isinstance的判断结果。其二定义__subclasshook__按结构自动认可# 适用于 Python 3.0from abc import ABC, abstractmethodclass Quacker(ABC):abstractmethoddef quack(self):...classmethoddef __subclasshook__(cls, C):if cls is Quacker:if any(quack in B.__dict__ for B in C.__mro__):return Truereturn NotImplementedclass Toy:def quack(self):return squeakprint(issubclass(Toy, Quacker)) # True__subclasshook__必须写成类方法返回值可以是True算作子类、False明确不算或NotImplemented交回默认机制继续判断。它由 ABCMeta 的__subclasscheck__调用因此isinstance也会跟着生效。如果想直接改isinstance的语义就在元类上定义__instancecheck__判断实例或__subclasscheck__判断子类# 适用于 Python 3.0class DuckMeta(type):def __instancecheck__(cls, instance):return hasattr(instance, quack)class Duck(metaclassDuckMeta):passclass Toy:def quack(self):return squeakprint(isinstance(Toy(), Duck)) # Trueprint(isinstance(42, Duck)) # False官方强调这两个方法在类的类型元类上查找不能定义成实际类里的类方法。这是它和普通方法最不一样的一点。版本提醒Python 2 里旧式类不继承object不参与这套元类与描述符机制元类要用类属性__metaclass__指定Python 3 改用metaclass关键字且所有类都是新式类。Python 2.7 已于2020 年 1 月 1 日停止维护新代码不必再为这些差异做兼容。三条路径的区别如下方式触发点是否需要类显式配合鸭子类型调用方自己 try /hasattr不需要根本不查类型ABC.register()被注册方显式调用一次要有人主动注册__subclasshook__issubclass/isinstance时按结构判断不需要但只能按结构判断元类__instancecheck__isinstance时不需要语义完全自定义四、元类 __call__拦截实例的产出__call__是元类里唯一能把控制权伸到「实例创建」的方法。除了单例它还能做对象池、缓存、访问计数等# 适用于 Python 3.0class Pooled(type):def __init__(cls, name, bases, namespace):super().__init__(name, bases, namespace)cls._pool []def __call__(cls, *args, **kwargs):if cls._pool:obj cls._pool.pop()obj.reset(*args, **kwargs) # 复用前先重置return objreturn super().__call__(*args, **kwargs)class Connection(metaclassPooled):def __init__(self, host):self.host hostdef reset(self, host):self.host hostc Connection(db.local)print(c.host) # db.local这里把池子初始化放在元类的__init__里cls._pool []因为每个类都要有自己独立的一份不能共用元类属性。复用对象时必须显式重置状态否则会带着上一次的数据——这是对象池最容易出 bug 的地方。同样地这套实现没有加锁多线程下不安全真要用得自己补同步。常见坑点在super().__new__之前读__abstractmethods__❌def __new__(mcls, name, bases, ns): if not cls.__abstractmethods__: ...; cls super().__new__(...)→ 值还没算出来判断全错。 ✅ 先cls super().__new__(...)之后再用getattr(cls, __abstractmethods__, None)。__prepare__忘记加classmethod❌def __prepare__(mcls, name, bases, **kw): ...→ 绑定规则不对第一个参数不是元类。 ✅ 加classmethod签名写成(mcls, name, bases, **kwargs)。自定义命名空间时把 dunder 也拦了❌if not key.startswith(_): raise ...之类反过来写或对大写判断没排除下划线。 →__module__/__qualname__等系统写入的键被误伤类构造失败。 ✅ 规则里显式放行下划线开头的名字。误以为register()会让类进入 MRO❌assert MyABC in tuple.__mro__注册后 → 官方明确说虚拟子类不出现在 MRO也不继承 ABC 的方法。 ✅ 只把它当「类型判断的开关」别指望能调用 ABC 里的方法。__instancecheck__里又调isinstance造成递归❌def __instancecheck__(cls, obj): return isinstance(obj, cls)→ 自我递归最终RecursionError。 ✅ 用具体判据如hasattr(obj, quack)或调用super().__instancecheck__(obj)。把__instancecheck__写成类里的方法❌class Duck: def __instancecheck__(self, obj): ...→ 官方说它在元类上查找写在类里不生效isinstance仍按默认规则走。 ✅ 定义在元类上或用abc.ABCMeta的__subclasshook__。__subclasshook__忘了返回NotImplemented❌ 不匹配时直接return False。 → 会强行否定一切连正常继承关系都被否掉。 ✅ 只在明确要否决时return False其余情况return NotImplemented交回默认机制。对象池/单例忘了线程安全与状态重置❌ 从池里取出对象就直接用不调reset。 → 带着上一次的属性继续用症状隐蔽。 ✅ 复用前显式重置多线程场景自己加锁别假设元类是安全的。总结进阶用法钩子要点与抽象基类协作继承ABCMeta 读__abstractmethods__必须在super().__new__之后读定制类命名空间__prepare__类方法返回值会被复制别当__dict__用虚拟子类register()/__subclasshook__不进 MRO__subclasshook__须返回NotImplemented兜底定制isinstance元类上的__instancecheck__/__subclasscheck__只能定义在元类上拦截实例产出元类__call__记得调super().__call__注意状态与并发这些用法的共同前提是你确实需要「在类创建或类调用这一层」介入。反过来如果只是想给子类自动加点行为__init_subclass__仍然是更省事的选择。最后再次强调那组区分鸭子类型是运行期「看着像就用」__subclasshook__是让显式类型检查「也认这个账」二者解决的问题不同不要互相替代。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑