Python笔记 - 利用装饰器设计注解体系
认识注解
注解(Annotation)是一种用于为代码添加元数据的机制。这些元数据可以在运行时被访问,用于为代码元素(如类、方法、字段等)提供额外的信息或指示。
由于Python中装饰器只能装饰类和方法,因此也只能为此二者提供额外的信息。
整体思路
通过Python中的装饰器实现注解功能,需要注意的是,此设计的缺陷在于只能装饰类和方法。
在Python中@语法糖,等价立即调用,参数为装饰的目标类或者函数:
classAnnotation:pass@Annotation等价
Annotation()由于Python作为动态语言,不涉及编译时期,我们去掉注解的声明周期,只提供作用范围,即作用在类上或者方法上,通过枚举类ElementType规范。
而对应的元注解为Target,即注解的注解,在Python中应该为装饰类装饰器的装饰器(有点绕)。
因为需要修改注解的原本行为,因此元注解的设计较为核心,当前阶段只涉及Target一个元注解,该注解应该具备如下功能:
- 接受一个ElementType作为参数,用于判断注解应该作用在类上还是函数上,或者皆可。
- 因为元注解的装饰对象一定是注解类(即类装饰器),因此元注解的目标对象为class类;所有注解类都应该默认实现Annotation接口。
- 由于Python中不具备Class类,即已知class A我们无法获取类A上是否存在注解,比如@AriaFor,因此元注解应该获取目标类A添加属性
__annotation_list__用于存储注解实例对象,方便通过类A获取元信息。
设计
1. 基类Annotation
参考Java注解概念,此接口默认为注解的超类,所有注解都属于Annotation。
classAnnotation:@abstractmethoddefannotationType(self)->'Annotation':raiseNotImplementedError2. 枚举类ElementType
此枚举类用于规范注解的作用范围,声明@Target时候必须指明作用范围,即@Target(ElementType.Type)。
classElementType(Enum):classType:def__init__(self,name,value,label):self.name=name self.value=value self.label=label TYPE=Type('type',type,'类')METHOD=Type('method',types.FunctionType,'函数')Q:为什么枚举类内部还需要封装一个Type类?
这是因为当我们进行类型检查的时候,我们需要判断装饰的目标对象是函数还是类,如果不是规定的类型,则应该抛出异常,而此类型则为Type.value。
3. 参数类型校验装饰器@accepts
此为函数装饰器,用于校验函数的入参是否符合规范,这里用来检查ElementType。
defaccepts(*types):defcheck_accepts(f):defwrapper(*args,**kwargs):ifnotall(isinstance(arg,types)forarginargs[1:]):raiseTypeError("Argument %s is not of type %s"%(args,types))returnf(*args,**kwargs)returnwrapperreturncheck_acceptsNote:此装饰器并不通用,原因是对args[1:]进行了截断,也就是通常针对类方法(self,*args)这种,因为我们不需要校验第一个参数self。
4. (核心)元注解@Target
此注解本质为类装饰器,作为注解的元注解,他需要改变注解的一些基本行为,主要包括三个方法的重写:__init__、__new__、__call__。
元注解只能作用在注解上,并且必须指明参数ElementType,否则无法正常工作。
4.1 源码
classTarget:@accepts(list,tuple,ElementType)def__init__(self,elementTypes:Union[List[ElementType],ElementType]):# 1. 检查列表或者元组类型是否正确ifisinstance(elementTypes,list)orisinstance(elementTypes,tuple):foreinelementTypes:ifnotisinstance(e,ElementType):raiseTypeError(f"@Target只能声明作用范围为ElementType,当前{type(e)}为不支持的类型。")# 2. 元组和ElementType需要转化为list列表形式ifisinstance(elementTypes,ElementType):elementTypes=[elementTypes]self.elementTypes=[e.valueforeinelementTypes]def__call__(self,cls):classAnnoProxy(cls,Annotation):defannotationType(self):returnclsdef__init__(self,*args,**kwargs):iflen(args)>1:raiseTypeError(f"@{self.__class__.__name__}只能接受一个args参数作为默认value,存在多个参数请使用字典。")iflen(args)==1:self.value=args[0]iflen(args)>0elsekwargs.get('value')super().__init__(*args,**kwargs)def__new__(_cls,*args,**kwargs):_instance=super(AnnoProxy,_cls).__new__(_cls)_instance.source=cls _instance.elementTypes=self.elementTypesiflen(kwargs)==0andlen(args)==1and(isinstance(args[0],type)orisinstance(args[0],types.FunctionType)):returnAnnoProxy.wrapper_target(_instance,args[0])else:# 其他情况则为 @AriaFor(123)或者@AriaFor(name="Tom")这种带参数的形式调用。_instance.elementTypes=self.elementTypesreturn_instancedef__call__(self,target):# 如果调用了__call__方法说明目标注解类的使用形式为带参数的类装饰器,即@AriaFor(123)这种# 此时 target 就是装饰对象,类或者函数returnAnnoProxy.wrapper_target(self,target)@staticmethoddefwrapper_target(_instance,target):support_types=[e.valueforein_instance.elementTypes]labels=[e.labelforein_instance.elementTypes]ifnotany(isinstance(target,s_type)fors_typeinsupport_types):raiseTypeError(f"@{_instance.source.__name__}无法装饰[{type(target).__name__}][{target.__name__}],此注解只能作用在[{'和'.join(labels)}]上")target.__annotation_list__.append(_instance)ifhasattr(target,'__annotation_list__')elsesetattr(target,'__annotation_list__',[_instance])returntargetreturnAnnoProxy4.2 说明
作为元注解,他的装饰对象是已知固定的,即注解类;并且一定是带参数的类装饰器,因此定义时候__init__方法接受固定类型参数ElementType。
当使用元注解装饰一个注解的时候,如下:
@Target(ElementType.Type)classAliaFor:pass应该固定返回一个代理类,即注解的代理类,该类的父类为目标类AliasFor,以及基类Annotation,因此__call__方法的返回值为注解的代理类,即AnnoProxy。
通过此代理类,我们修改注解的基本行为,主要通过定义:__init__、__new__和__call__方法。
由于我们在使用注解的时候存在两种情况,这两种情况在类装饰器中的表现完全不同,因此必须分开讨论:
使用方式一:
@AliasForclassA:pass使用方式二:
@AliasFor(123)classA:pass# 或者@AliasFor(name="Tom")classA:pass方式一是不带参数的类装饰器,此方法执行目标类A作为参数传递的时候作为__new__方法的参数传入。
方式二则是带参数的类装饰器,此方法执行目标类A讲作为参数传递的时候作为__call__方法的参数传入。
效果展示
当我们定义注解的时候,我们只需要通过元注解@Target并且指定作用范围即可,可以是单个ElementType,也可以同时支持类和函数。
当携带参数时,即注解的属性值,如果属性值只有一个,并且名称为value,可以省略不写。
1. 不带参数的使用
声明一个注解:
@Target(ElementType.TYPE)classAliaFor:pass使用注解:
@AliaForclassA:passif__name__=='__main__':# 获取类A上的注解信息forainA.__annotation_list__:# 查看注解实例print(a)# 查看注解的类型print(a.annotationType())# 查看注解是否为Annotation的实例对象print(isinstance(a,Annotation))输出:
可以看到,返回的是一个代理类对象,即AnnoProxy,且属于Annotation类。
2. 带参数的使用
2.1 只有一个参数,且参数名称为value
声明一个注解:
@Target(ElementType.TYPE)classAliaFor:def__init__(self,value):self.value=value使用注解:
@AliaFor(123)classA:passif__name__=='__main__':# 获取类A上的注解信息forainA.__annotation_list__:# 查看注解实例print(a)# 查看注解的类型print(a.annotationType())# 查看注解是否为Annotation的实例对象print(isinstance(a,Annotation))# 查看注解的属性值print(a.value)输出:
2.2 注解属性值
声明一个注解:
@Target(ElementType.TYPE)classAliaFor:def__init__(self,name,age):self.name=name self.age=age使用注解:
@AliaFor(name="Tom",age=18)classA:passif__name__=='__main__':# 获取类A上的注解信息forainA.__annotation_list__:# 查看注解实例print(a)# 查看注解的类型print(a.annotationType())# 查看注解是否为Annotation的实例对象print(isinstance(a,Annotation))# 查看注解的属性值print(a.name)print(a.age)输出:
3. 错误作用范围异常
声明注解作用在类上:
@Target(ElementType.TYPE)classAliaFor:pass错误的使用:
classA:@AliaFordefadd(self):pass输出:
4. 使用工具类
classAnnotationUtils:@staticmethoddefgetAnnotations(source:type)->Union[List[Annotation],None]:returnsource.__annotation_list__ifhasattr(source,'__annotation_list__')elseNone@staticmethoddefgetAnnotation(source:type,annotation_type:type)->Union[Annotation,None]:ifAnnotationUtils.getAnnotations(source)isNone:returnNonereturnnext((aforainAnnotationUtils.getAnnotations(source)ifisinstance(a,annotation_type)),None)@staticmethoddefisAnnotationPresent(source:type,annotation_type:type):returnAnnotationUtils.getAnnotation(source,annotation_type)isnotNone@staticmethoddefgetAnnotationAttributes(annotation:Annotation)->dict:return{k:vfork,vinannotation.__dict__.items()ifnotk.startswith('_')}@staticmethoddefgetAnnotationAttribute(annotation:Annotation,attribute_name:str):returnAnnotationUtils.getAnnotationAttributes(annotation).get(attribute_name,None)声明一个注解:
@Target([ElementType.TYPE,ElementType.METHOD])classAliaFor:def__init__(self,name,age):self.name=name self.age=age使用注解:
@AliaFor(name="Tom",age=18)classA:@AliaFordefadd(self):passif__name__=='__main__':print(AnnotationUtils.getAnnotations(A))print(AnnotationUtils.getAnnotation(A,AliaFor))print(AnnotationUtils.isAnnotationPresent(A,AliaFor))print(AnnotationUtils.getAnnotationAttributes(AnnotationUtils.getAnnotation(A,AliaFor)))print(AnnotationUtils.getAnnotationAttribute(AnnotationUtils.getAnnotation(A,AliaFor),'name'))输出:
🔗参考链接
[1]:官方文档函数装饰器 PEP 318
[2]:官方文档类装饰器 PEP 3129
[3]:博客 # # Python笔记 - 函数、方法和类装饰器
