元类

from abc import ABCMeta, abstractmethod
import logging
from functools import wraps


# 关键:配置日志级别,否则info不输出
logging.basicConfig(
    level=logging.INFO,
    format="%(name)s: %(message)s"
)


class DieMeta(ABCMeta): # 当你写 class 定义的时候,执行这行代码时,Python 会调用元类的`__new__`去构造这个类
    def __new__(mcls, name, bases, namespace, /, **kwargs):
        """类被定义的时候执行__new__,不是实例化对象的时候,并且只在定义类的时候跑1次

        Args:
            mcls (_type_): 元类本身,这里就是DieMeta这个类
            name (_type_): 正在被创建的类的类名,这里是BaseDie,Dice
            bases (_type_): 正在创建类的所有父类,如果(object, )
            namespace (_type_): 字典类型,类命名空间,包括类内部所有变量,函数,方法
        """
        """
        "roll" in namespace:判断当前待创建的类的命名空间字典里是否有叫roll的方法
        getattr(namespace["roll"], "__isabstractmethod__", False):获取roll对象是否有__isabstractmethod__属性,没有则返回默认值False
            @abstracemethod装饰器会给函数打上属性__isabstractmethod__=True
            如果roll是抽象方法,则返回True
        
        not getattr(namespace["roll"], "__isabstractmethod__", False): 表示roll是普通方法
        
        namespace.setdefault("logger", logging.getLogger(name)): 给类命名空间字典增加一个logger属性,name表示当前类名字
        
        riginal_method = namespace["roll"]:将原始roll函数拿出来保存,后面装饰器内部调用原始逻辑
        
        @wraps(original_method):复制原roll函数的__name__,__doc__元信息到logged_rool
        def logged_roll(self)::替换后的新方法,self是未来类的实例对象
        original_method(self):调用原来真正roll方法,执行业务逻辑
        self.logger.info(f"Rolled {self.face}"):执行完roll之后打印日志,读取实例属性face
        ----------到这一步只是定义函数,还没有执行,函数对象存入内存----------------
        
        amespace["roll"] = logged_roll:原地修改类命名空间字典,把原来roll函数替换成包装后的logged_roll函数
        -----------此时namespace字典里面roll已经变成带日志增强版本,所有这一切操作,都发生在类被创建阶段,还没有任何实例对象----------
        
        new_object = super().__new__(mcls, name, bases,namespace):调用父元类ABCMeta的__new__,真正构造出class类对象
        new_object : 构造完成的类对象
        ------------把处理完毕的namespace(roll已经被替换,logger属性已经加入)交给ABCMeta,生成最终类------------
        """
        if "roll" in namespace and not getattr(namespace["roll"], "__isabstractmethod__", False):
            namespace.setdefault("logger", logging.getLogger(name))
            original_method = namespace["roll"]
            
            @wraps(original_method)
            def logged_roll(self):
                original_method(self)
                self.logger.info(f"Rolled {self.face}")
            
            namespace["roll"] = logged_roll
        
        new_object = super().__new__(mcls, name, bases,namespace)

        return new_object
    

# 基类,绑定元类,对外暴露给用户继承
class BaseDie(metaclass=DieMeta):
    @abstractmethod
    def roll(self):
        raise NotImplementedError

class Dice(BaseDie):
    face = 0

    def roll(self):
        self.face = 6


if __name__ == '__main__':
    """四个阶段
    阶段一:解释器读取`class Dice(BaseDie),BaseDie的元类是DieMeta,python自动让Dice的元类=DieMeta:`类定义块
        只要子类继承`BaseDie`,元类`DieMeta.__new__`就会执行,自动包装 roll 方法
        1.python执行类内部所有代码:face=0, def roll(self)...
        2.把全部内容收集到namespace字典
            namespace = {
                'face': 0,
                'roll': 原始roll函数对象
            }
        3.触发元类DieMeta.new(mcls=DieMeta,name='Dice',bases=(),namespace={...})
        4.判断: namespace有roll,并且roll不是抽象方法,条件true
        5.namespace.setdefault增加logger键到namespace字典
        6.保存origin_method = 原始roll函数
        7.创建logged_roll包装函数,@wraps复制元信息
        8.namespace['roll'] = logged_roll,覆盖roll
        9.调用父元类ABCMeta.__new__,传入修改好的namespce,生成Dice类对象
        10.return new_object,变量Dice指向这个类对象
    
    阶段二:
        1.创建BaseDie基类,绑定DieMeta元类,对外暴露给用户继承
        2.`BaseDie`里面`roll`是`@abstractmethod`,带`__isabstractmethod__ = True`
        3.元类`DieMeta.__new__`处理`BaseDie`类的时候:`if`条件不成立,**不会包装 BaseDie 的 roll**。
        4.处理子类`Dice`的时候:Dice 自己 namespace 里面有普通 roll 实现,不是抽象方法 → 触发包装、注入 logger、替换 roll。

    
    阶段三:d = Dice() 实例化对象
        1.python调用类的__call__,元类继承链上type.__call__
        2.内部执行:Dice.__new__()创建实例对象,再调用Dice.__init__初始化
        3.返回实例d
        元类DieMeta的__new__不会再被运行!元类__new__只在定义类的时候跑1次
    
    阶段四:调用d.roll()
        1.d实例查找roll方法,找到类上已经被替换的logged_roll(self)
        2.进入logged_roll,self就是d实例
        3.original_method(self)调用原始roll函数,执行self.face = 6,实例d的face属性变成了6
        4.self.logger.info(f"Rolled {self.face}")拿实例d的face值打印日志
        5.函数返回
    
    """
    d = Dice()
    d.roll()
posted @ 2026-09-09 23:38  我在路上回头看  阅读(12)  评论(0)    收藏  举报