Python笔记 - 利用装饰器设计注解体系

news2024/11/16 3:19:59

认识注解

注解(Annotation)是一种用于为代码添加元数据的机制。这些元数据可以在运行时被访问,用于为代码元素(如类、方法、字段等)提供额外的信息或指示。

由于Python中装饰器只能装饰类和方法,因此也只能为此二者提供额外的信息。

整体思路

通过Python中的装饰器实现注解功能,需要注意的是,此设计的缺陷在于只能装饰类和方法。

在Python中@语法糖,等价立即调用,参数为装饰的目标类或者函数:

class Annotation:
	pass

@Annotation

等价

Annotation()

由于Python作为动态语言,不涉及编译时期,我们去掉注解的声明周期,只提供作用范围,即作用在类上或者方法上,通过枚举类ElementType规范。

而对应的元注解为Target,即注解的注解,在Python中应该为装饰类装饰器的装饰器(有点绕)。

因为需要修改注解的原本行为,因此元注解的设计较为核心,当前阶段只涉及Target一个元注解,该注解应该具备如下功能:

  1. 接受一个ElementType作为参数,用于判断注解应该作用在类上还是函数上,或者皆可。
  2. 因为元注解的装饰对象一定是注解类(即类装饰器),因此元注解的目标对象为class类;所有注解类都应该默认实现Annotation接口。
  3. 由于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笔记 - 函数、方法和类装饰器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2185102.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C动态内存管理

前言:不知不觉又过去了很长的一段时间。今天对C语言中的动态内存管理进行一个系统性的总结。 1 为什么要有动态内存分配 在C语言中,使用int,float,double,short等数据内置类型以及数组不是也可以开辟内存空间吗&…

《算法岗面试宝典》重磅发布!

大家好,历时半年完善,《算法岗面试宝典》 终于可以跟大家见面了。 最近 ChatGPT 爆火,推动了技术圈对大模型算法场景落地的热情,就业市场招聘人数越来越多,算法岗一跃成为竞争难度第一的岗位。 岗位方向 从细分方向…

李宏毅深度学习-梯度下降和Normalization归一化

Gradient Descent梯度下降 ▽ -> 梯度gradient -> vector向量 -> 下图中的红色箭头(loss等高线的法线方向) Tip1: Tuning your learning rates Adaptive Learning Rates自适应 通常lr会越来越小 Adaptive Learning Rates中每个参数都给它不同…

110.WEB渗透测试-信息收集-ARL(1)

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 内容参考于: 易锦网校会员专享课 上一个内容:109.WEB渗透测试-信息收集-FOFA语法(9) 信息收集自动化工具-灯塔…

黑马头条day6-kafka及异步通知文章上下架

今天任务比较水 主要是kafka入门和 文章上下架 以及异步通知article同步到app的前端数据 需要重新看一下(使用步骤并不是很复杂 kafka主要解决高并发) 1 kafka的入门 和 使用异步 需要重新看一下了流程和 详细信息 2 bug 打开app页面的时候出现503 服…

从0到1深入浅出构建Nest.Js项目

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对…

动手学运动规划: 2.2.c 3次样条曲线代码解析

学习? 学个P!☺ — 亮剑 李云龙 🏰代码及环境配置:请参考 环境配置和代码运行! 本节提供了3次样条曲线的代码测试 python3 tests/curves/cubic_spline.py2.2.c.1 3次样条曲线代码实现 CubicSpline1D实现了1维的3次样条曲线, 需要输入一组离散点. Cub…

现在别买理想L7/L8,问界M8要来暴揍友商了

文 | AUTO芯球 作者 | 雷慢 问界又一重磅炸弹要来了, 它就是问界M8, 来看,M8刚又曝光了大量谍照。 现在我打听的消息是这样的, 11月广州车展亮相预售, 12月底正式上市,25年春节前后开始交付&#xff…

计算机网络:计算机网络体系结构 —— 专用术语总结

文章目录 专用术语实体协议服务服务访问点 SAP 服务原语 SP 协议数据单元 PDU服务数据单元 SDU 专用术语 实体 实体是指任何可以发送或接收信息的硬件或软件进程 对等实体是指通信双方处于相同层次中的实体,如通信双方应用层的浏览器进程和 Web 服务器进程。 协…

Java组件化开发:jar包

我在java基础:原始数据类型,包的创建与导入-CSDN博客一文中记录了包的使用,此文就详细讲解一下IDEA中如何进行组件化开发。 介绍 现在的软件系统功能越来越复杂,规模也越来越大,为了应对这种挑战,人们将“…

深入解析Python错误消息及解决方法

深入解析Python错误消息及解决方法 Python是开发者广泛使用的语言,因其简洁的语法和强大的标准库而深受欢迎。然而,Python程序在运行过程中,错误不可避免。理解Python的错误消息并正确处理这些错误,是提升代码质量和调试效率的重…

3.点位管理改造-列表查询——帝可得管理系统

目录 前言一、与页面原型差距1.现在:2.目标:3. 存在问题:所在区域和合作商ID展示的都是ID,而不是名称;同时合作商ID应改为合作商 二、修改1.重新设计SQL语句2.修改mapper层,使用Mybatis中的嵌套查询3.修改s…

AI人工智能人像修饰中文面板PS插件 Retouch Pro 3.2.0 中文汉化版

AI人工智能人像修饰PS扩展插件 Retouch Pro 3.2.0 中文汉化版 支持软件:PS 2018 – PS 2025或更高版本 系统要求:Windows系统 或 MacOS系统 出处:https://www.aeown.com/thread-3061-1-1.html Retouch Pro Panel 有一个非常强大和先进的人工…

Python Tips6 基于数据库和钉钉机器人的通知

说明 起因是我第一版quant程序的短信通知失效了。最初认为短信是比较即时且比较醒目的通知方式,现在看来完全不行。 列举三个主要问题: 1 延时。在早先还能收到消息的时候,迟滞就很严重,几分钟都算短的。2 完全丢失。我手机没有…

ACP科普:SoSM和CPO

在Scrum of Scrums(SoS)框架中,SoSM(Scrum of Scrums Master)和CPO(Chief Product Owner)是两个关键角色,负责协调多个Scrum团队的工作,确保项目的顺利进行。以下是对这两…

Android AMS介绍

注:本文为作者学习笔记,如有误,请各位大佬指点 系统进程运行环境的初始化 Context是一个抽象类,它可以访问application环境的全局信息和各种资源信息和类 context功能: 对Activity、Service生命周期的管理通过Intent发…

c++进阶之多态讲解

这篇文章和大家一起学习一下c中的多态 多态的概念 多态的概念:通俗来讲,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。 什么是静态多态 前⾯讲的函数重载和函数模板,它们传不同类型的参数就可以调用不同的函数&…

深入理解 C 语言中的内存操作函数:memcpy、memmove、memset 和 memcmp

目录: 前言一、 memcpy 函数二、 memmove 函数三、 memset 函数四、 memcmp 函数总结 前言 在 C 语言中,内存操作函数是非常重要的工具,它们允许我们对内存进行直接操作,从而实现高效的数据处理。本文将深入探讨四个常用的内存操…

zabbix7.0web页面删除主机操作实现过程

前言 服务端配置 链接: rocky9.2部署zabbix服务端的详细过程 被监控端配置 链接: zabbix7.0监控linux主机案例详解 环境 主机ip应用zabbix-server192.168.10.11zabbix本体zabbix-client192.168.10.12zabbix-agent zabbix-server(服务端已配置) zabbix-client(被监控端已配置…

Bruno:拥有 11.2k star 的免费开源 API 测试工具

Github 开源地址: https://github.com/usebruno/bruno 官网地址: https://www.usebruno.com/ 下载地址: https://www.usebruno.com/downloads 使用文档: https://docs.usebruno.com/ Bruno 是一款全新且创新的 API 客户端&…