元类
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()
-------------------------------------------
个性签名:代码过万,键盘敲烂!!!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!

浙公网安备 33010602011771号