认识注解
注解(Annotation)是一种用于为代码添加元数据的机制。这些元数据可以在运行时被访问,用于为代码元素(如类、方法、字段等)提供额外的信息或指示。
由于Python中装饰器只能装饰类和方法,因此也只能为此二者提供额外的信息。
整体思路
通过Python中的装饰器实现注解功能,需要注意的是,此设计的缺陷在于只能装饰类和方法。
在Python中@语法糖,等价立即调用,参数为装饰的目标类或者函数:
class Annotation:
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。
class Annotation:
@abstractmethod
def annotationType(self) -> 'Annotation':
raise NotImplementedError
2. 枚举类ElementType
此枚举类用于规范注解的作用范围,声明@Target时候必须指明作用范围,即@Target(ElementType.Type)。
class ElementType(Enum):
class Type:
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。
def accepts(*types):
def check_accepts(f):
def wrapper(*args, **kwargs):
if not all(isinstance(arg, types) for arg in args[1:]):
raise TypeError("Argument %s is not of type %s" % (args, types))
return f(*args, **kwargs)
return wrapper
return check_accepts
Note:此装饰器并不通用,原因是对args[1:]进行了截断,也就是通常针对类方法(self,*args)
这种,因为我们不需要校验第一个参数self。
4. (核心)元注解@Target
此注解本质为类装饰器,作为注解的元注解,他需要改变注解的一些基本行为,主要包括三个方法的重写:__init__
、__new__
、__call__
。
元注解只能作用在注解上,并且必须指明参数ElementType,否则无法正常工作。
4.1 源码
class Target:
@accepts(list, tuple, ElementType)
def __init__(self, elementTypes: Union[List[ElementType], ElementType]):
# 1. 检查列表或者元组类型是否正确
if isinstance(elementTypes, list) or isinstance(elementTypes, tuple):
for e in elementTypes:
if not isinstance(e, ElementType):
raise TypeError(f"@Target只能声明作用范围为ElementType,当前{type(e)}为不支持的类型。")
# 2. 元组和ElementType需要转化为list列表形式
if isinstance(elementTypes, ElementType):
elementTypes = [elementTypes]
self.elementTypes = [e.value for e in elementTypes]
def __call__(self, cls):
class AnnoProxy(cls, Annotation):
def annotationType(self):
return cls
def __init__(self, *args, **kwargs):
if len(args) > 1:
raise TypeError(
f"@{self.__class__.__name__}只能接受一个args参数作为默认value,存在多个参数请使用字典。")
if len(args) == 1:
self.value = args[0] if len(args) > 0 else kwargs.get('value')
super().__init__(*args, **kwargs)
def __new__(_cls, *args, **kwargs):
_instance = super(AnnoProxy, _cls).__new__(_cls)
_instance.source = cls
_instance.elementTypes = self.elementTypes
if len(kwargs) == 0 and len(args) == 1 and (isinstance(args[0], type) or isinstance(args[0],
types.FunctionType)):
return AnnoProxy.wrapper_target(_instance, args[0])
else:
# 其他情况则为 @AriaFor(123)或者@AriaFor(name="Tom")这种带参数的形式调用。
_instance.elementTypes = self.elementTypes
return _instance
def __call__(self, target):
# 如果调用了__call__方法说明目标注解类的使用形式为带参数的类装饰器,即@AriaFor(123)这种
# 此时 target 就是装饰对象,类或者函数
return AnnoProxy.wrapper_target(self, target)
@staticmethod
def wrapper_target(_instance, target):
support_types = [e.value for e in _instance.elementTypes]
labels = [e.label for e in _instance.elementTypes]
if not any(isinstance(target, s_type) for s_type in support_types):
raise TypeError(
f"@{_instance.source.__name__}无法装饰[{type(target).__name__}][{target.__name__}],此注解只能作用在[{'和'.join(labels)}]上")
target.__annotation_list__.append(_instance) if hasattr(target, '__annotation_list__') else setattr(target,
'__annotation_list__',
[_instance])
return target
return AnnoProxy
4.2 说明
作为元注解,他的装饰对象是已知固定的,即注解类;并且一定是带参数的类装饰器,因此定义时候__init__
方法接受固定类型参数ElementType。
当使用元注解装饰一个注解的时候,如下:
@Target(ElementType.Type)
class AliaFor:
pass
应该固定返回一个代理类,即注解的代理类,该类的父类为目标类AliasFor,以及基类Annotation,因此__call__
方法的返回值为注解的代理类,即AnnoProxy。
通过此代理类,我们修改注解的基本行为,主要通过定义:__init__
、__new__
和__call__
方法。
由于我们在使用注解的时候存在两种情况,这两种情况在类装饰器中的表现完全不同,因此必须分开讨论:
使用方式一:
@AliasFor
class A:
pass
使用方式二:
@AliasFor(123)
class A:
pass
# 或者
@AliasFor(name="Tom")
class A:
pass
方式一是不带参数的类装饰器,此方法执行目标类A作为参数传递的时候作为__new__方法的参数传入。
方式二则是带参数的类装饰器,此方法执行目标类A讲作为参数传递的时候作为__call__方法的参数传入。
效果展示
当我们定义注解的时候,我们只需要通过元注解@Target并且指定作用范围即可,可以是单个ElementType,也可以同时支持类和函数。
当携带参数时,即注解的属性值,如果属性值只有一个,并且名称为value,可以省略不写。
1. 不带参数的使用
声明一个注解:
@Target(ElementType.TYPE)
class AliaFor:
pass
使用注解:
@AliaFor
class A:
pass
if __name__ == '__main__':
# 获取类A上的注解信息
for a in A.__annotation_list__:
# 查看注解实例
print(a)
# 查看注解的类型
print(a.annotationType())
# 查看注解是否为Annotation的实例对象
print(isinstance(a, Annotation))
输出:
可以看到,返回的是一个代理类对象,即AnnoProxy,且属于Annotation类。
2. 带参数的使用
2.1 只有一个参数,且参数名称为value
声明一个注解:
@Target(ElementType.TYPE)
class AliaFor:
def __init__(self, value):
self.value = value
使用注解:
@AliaFor(123)
class A:
pass
if __name__ == '__main__':
# 获取类A上的注解信息
for a in A.__annotation_list__:
# 查看注解实例
print(a)
# 查看注解的类型
print(a.annotationType())
# 查看注解是否为Annotation的实例对象
print(isinstance(a, Annotation))
# 查看注解的属性值
print(a.value)
输出:
2.2 注解属性值
声明一个注解:
@Target(ElementType.TYPE)
class AliaFor:
def __init__(self, name, age):
self.name = name
self.age = age
使用注解:
@AliaFor(name="Tom", age=18)
class A:
pass
if __name__ == '__main__':
# 获取类A上的注解信息
for a in A.__annotation_list__:
# 查看注解实例
print(a)
# 查看注解的类型
print(a.annotationType())
# 查看注解是否为Annotation的实例对象
print(isinstance(a, Annotation))
# 查看注解的属性值
print(a.name)
print(a.age)
输出:
3. 错误作用范围异常
声明注解作用在类上:
@Target(ElementType.TYPE)
class AliaFor:
pass
错误的使用:
class A:
@AliaFor
def add(self):
pass
输出:
4. 使用工具类
class AnnotationUtils:
@staticmethod
def getAnnotations(source: type) -> Union[List[Annotation], None]:
return source.__annotation_list__ if hasattr(source, '__annotation_list__') else None
@staticmethod
def getAnnotation(source: type, annotation_type: type) -> Union[Annotation, None]:
if AnnotationUtils.getAnnotations(source) is None:
return None
return next((a for a in AnnotationUtils.getAnnotations(source) if isinstance(a, annotation_type)), None)
@staticmethod
def isAnnotationPresent(source: type, annotation_type: type):
return AnnotationUtils.getAnnotation(source, annotation_type) is not None
@staticmethod
def getAnnotationAttributes(annotation: Annotation) -> dict:
return {k: v for k, v in annotation.__dict__.items() if not k.startswith('_')}
@staticmethod
def getAnnotationAttribute(annotation: Annotation, attribute_name: str):
return AnnotationUtils.getAnnotationAttributes(annotation).get(attribute_name, None)
声明一个注解:
@Target([ElementType.TYPE, ElementType.METHOD])
class AliaFor:
def __init__(self, name, age):
self.name = name
self.age = age
使用注解:
@AliaFor(name="Tom", age=18)
class A:
@AliaFor
def add(self):
pass
if __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笔记 - 函数、方法和类装饰器