ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python 装饰器实战:手写 @logged 日志装饰器与 @validated 类型校验装饰器(Advanced Python Mastery 练习 7.1)

Python 装饰器实战:手写 @logged 日志装饰器与 @validated 类型校验装饰器(Advanced Python Mastery 练习 7.1) 示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载导读本文基于 Advanced Python Mastery作者 David Beazley仓库python-mastery第 7 章「元编程」的首个练习 Exercises/ex7_1.md 展开。你将亲手实现两个装饰器一个每次调用函数时打印日志的logged以及一个基于函数注解自动校验参数与返回值类型的validated。读完本文你不仅能理解def wrapper(*args, **kwargs)这一经典装饰器形态还能掌握用inspect.signature().bind()__annotations__把类型校验器接入函数调用的完整技术路线并看到它与练习 6.5 中可调用对象ValidatedFunction的演变关系。1. 练习定位装饰器从哪来要到哪里去练习 7.1 位于课程第 7 章Metaprogramming元编程其官方目标只有一句话学会定义简单的装饰器函数simple decorator functions。它由两个步骤组成(a) 你的第一个装饰器写一个极简的logged装饰器在每次函数被调用时打印一条消息(b) 一个真正的装饰器把练习 6.5 中为validate.py写的可调用类ValidatedFunction改写成装饰器函数validated让它能通过validated语法直接作用于函数与方法。整个练习新建logcall.py、修改validate.py官方提示「这一部分代码量不大但底层有很多琐碎细节low-level fiddly bits最终解法与练习 6.5 几乎一样」。值得强调的是练习 6.5 已经实现过一个「看起来像函数、但其实是对象」的ValidatedFunction可调用对象而练习 7.1 的目标是把它升级为真正的装饰器语法让validated能干净地用在函数和方法上——这也是下一节练习 7.2装饰器链、带参数的装饰器、元数据保留的铺垫。2. (a) 你的第一个装饰器logged2.1 代码实现按照练习要求新建logcall.py写入# logcall.py def logged(func): print(Adding logging to, func.__name__) def wrapper(*args, **kwargs): print(Calling, func.__name__) return func(*args, **kwargs) return wrapper这段代码麻雀虽小五脏俱全它包含了装饰器的全部三个要素外层函数接收被装饰函数logged(func)的参数func就是被logged修饰的那个函数对象内层wrapper负责替换原函数调用方拿到的其实是wrapper它先打印日志再调用真正的func(*args, **kwargs)并把结果原样返回返回值是被包装后的函数logged返回wrapper从而完成把函数 A 替换成函数 B的替换动作。*args, **kwargs保证了wrapper能接收任意位置参数和关键字参数从而适配任意签名的函数——这是装饰器「透明转发」的标配写法。2.2 应用与验证练习要求再建一个sample.py把logged应用到几个函数上# sample.py from logcall import logged logged def add(x,y): return xy logged def sub(x,y): return x-y注意一个容易被忽视的细节print(Adding logging to, func.__name__)位于logged的函数体顶层只在模块被导入即logged装饰器执行时打印一次而print(Calling, func.__name__)位于wrapper内部每次函数被调用都会打印。两者的时机完全不同。按练习给出的交互式会话验证 import sample Adding logging to add Adding logging to sub sample.add(3,4) Calling add 7 sample.sub(2,3) Calling sub -1 执行流程拆解import sample时模块顶层依次执行add logged(add)、sub logged(sub)所以立刻打印两行 Adding logging to ...sample.add(3,4)实际调用的是wrapper(3,4)先打印 Calling add再执行add(3,4)返回7。仓库中该练习的完整参考实现位于 Solutions/7_1/logcall.py 与 Solutions/7_1/sample.py两者与练习要求逐字对应。3. (b) 真正的装饰器validated3.1 从可调用对象到装饰器为什么需要这一步在练习 6.5 中我们先用ValidatedFunction类实现了注解校验其用法是def add(x: Integer, y:Integer) - Integer: return x y add ValidatedFunction(add) # 手动替换这种方法有一个致命缺陷——在类的方法上几乎无法使用。因为sell ValidatedFunction(sell)会把一个普通函数替换为自定义可调用对象Python 在类体里不会把它当作描述符descriptor处理导致self绑定失败调用s.sell(10)时报错TypeError: missing a required argument: nshares详见 练习 6.5 的 (c) 挑战部分。而装饰器函数validated在方法上的表现则完全不同validated生成的wrapper依然是普通函数self的绑定机制照常工作因此无需任何额外处理就能用在类方法上——这正是练习 7.1 第二部分的核心价值也是官方注释「thevalidateddecorator should work in classes (you dont need to do anything special)」的含义。3.2 期望的调用形态练习要求改写出的validated应支持如下写法注意注解值是Integer、PositiveInteger这样的校验器类而不是intfrom validate import Integer, validated validated def add(x: Integer, y:Integer) - Integer: return x y validated def pow(x: Integer, y:Integer) - Integer: return x ** y3.3 期望的行为与异常信息装饰后的函数行为如下——注意练习特别要求把异常修补patch up成更有用的信息 add(2, 3) 5 add(2, 3) Traceback (most recent call last): File stdin, line 1, in module File validate.py, line 75, in wrapper raise TypeError(Bad Arguments\n \n.join(errors)) TypeError: Bad Arguments x: Expected class int y: Expected class int pow(2, 3) 8 pow(2, -1) Traceback (most recent call last): File stdin, line 1, in module File validate.py, line 83, in wrapper raise TypeError(fBad return: {e}) from None TypeError: Bad return: Expected class int 这里隐藏着三个工程要点参数错误聚合add(2, 3)时x、y两个参数的校验都失败异常信息把两条错误合并为一次TypeError而非报第一个错误就中断便于使用者一次性看到全部问题错误格式统一每个参数错误缩进两格x: Expected class int与Bad Arguments\n \n.join(errors)的拼接方式一一对应返回值独立校验pow(2, -1)的参数2和-1都是整数、参数校验通过但返回值2 ** -1 0.5不是整数于是触发返回检查抛出的信息是Bad return: Expected class int并且用from None抑制了原始异常的上下文链避免出现丑陋的 During handling of the above exception... 级联输出。3.4 类方法中的使用练习还要求验证validated在类中可以直接使用无需特殊处理class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price property def cost(self): return self.shares * self.price validated def sell(self, nshares:PositiveInteger): self.shares - nshares在sell方法中注解PositiveInteger会拦截负数与小数保证self.shares - nshares不会把持仓减成非法状态。4. 源码级解析仓库中的完整validated实现4.1 前置依赖Validator校验器家族validated依赖练习 4.3 建立的校验器类体系。在 Solutions/7_1/validate.py 中可以看到完整的层次结构class Validator: def __init__(self, nameNone): self.name name def __set_name__(self, cls, name): self.name name classmethod def check(cls, value): return value def __set__(self, instance, value): instance.__dict__[self.name] self.check(value) class Typed(Validator): expected_type object classmethod def check(cls, value): if not isinstance(value, cls.expected_type): raise TypeError(fexpected {cls.expected_type}) return super().check(value) class Integer(Typed): expected_type int class Float(Typed): expected_type float class String(Typed): expected_type str class Positive(Validator): classmethod def check(cls, value): if value 0: raise ValueError(must be 0) return super().check(value) class NonEmpty(Validator): classmethod def check(cls, value): if len(value) 0: raise ValueError(must be non-empty) return super().check(value) class PositiveInteger(Integer, Positive): pass class PositiveFloat(Float, Positive): pass class NonEmptyString(String, NonEmpty): pass关键机制Validator的check是类方法所以Integer.check(hello)无需实例化即可直接调用PositiveInteger通过多重继承同时获得「类型必须为int」和「值必须 0」两层约束——Integer.check先跑isinstance检查Positive.check后跑非负检查。__set__与__set_name__则服务于把校验器作为类属性描述符用在Stock.name NonEmptyString()这类场景属于练习 4.2/4.3 的延伸能力与本文的validated无直接依赖。4.2 完整的validated装饰器练习 7.1 的官方解法完整代码见 Exercises/soln7_1.md而仓库 Solutions/7_1/validate.py 给出了加入了isvalidator过滤的增强版。两者主干一致下面以仓库版本为准逐步拆解from inspect import signature def isvalidator(item): return isinstance(item, type) and issubclass(item, Validator) def validated(func): sig signature(func) # Gather the function annotations annotations { name:val for name, val in func.__annotations__.items() if isvalidator(val) } # Get the return annotation (if any) retcheck annotations.pop(return, None) def wrapper(*args, **kwargs): bound sig.bind(*args, **kwargs) errors [] # Enforce argument checks for name, validator in annotations.items(): try: validator.check(bound.arguments[name]) except Exception as e: errors.append(f {name}: {e}) if errors: raise TypeError(Bad Arguments\n \n.join(errors)) result func(*args, **kwargs) # Enforce return check (if any) if retcheck: try: retcheck.check(result) except Exception as e: raise TypeError(fBad return: {e}) from None return result return wrapper4.3 逐段解读装饰器的五脏六腑① 收集签名sig signature(func)用inspect.signature()捕获函数签名。后续sig.bind(*args, **kwargs)能把调用时的实际参数按名字绑定得到BoundArguments对象从而通过bound.arguments[name]按参数名取到传入值。这是「注解与实参对齐」的关键——否则wrapper只能拿到*args的位置序列无法把注解x: Integer与实参2一一对应。② 过滤注解只保留校验器类func.__annotations__是{x: Integer, y: Integer, return: Integer}这样的字典。仓库版本用isvalidator(val)过滤只保留Validator子类避免把int、str等普通注解误当校验器。随后annotations.pop(return, None)把返回类型单独取出存为retcheck——注意pop用的是默认值None允许函数没有返回注解。③ 参数校验与错误聚合wrapper先sig.bind(*args, **kwargs)绑定实参若参数个数不匹配bind本身就会抛TypeError然后遍历每个(name, validator)用validator.check(bound.arguments[name])校验实参任一校验失败就把f {name}: {e}追加进errors列表。若errors非空一次性抛出raise TypeError(Bad Arguments\n \n.join(errors))这解释了交互式会话中那条包含两行x:、y:的Bad Arguments错误——它其实是errors列表的逐行拼接。④ 执行原函数只有参数全部校验通过才调用result func(*args, **kwargs)真正执行业务逻辑。这与「先校验后执行」的防御式编程理念一致。⑤ 返回值校验与异常链抑制若存在retcheck对结果做同样检查失败时raise TypeError(fBad return: {e}) from Nonefrom None是练习明确要求的补丁之一它把Bad return: Expected class int作为唯一的异常呈现屏蔽了底层check抛出的原始异常细节让报错聚焦于「返回值不合法」这一最终事实。⑥ 返回wrappervalidated最终返回wrapper完成函数替换。由于wrapper是普通函数Python 描述符协议__get__照常工作所以它作为类方法使用时self能正常绑定——这正是本文 3.1 节强调的「无需特殊处理」的底层原因。4.4 与练习 6.5ValidatedFunction的差异对照对比 Solutions/6_5/validate.py 中旧版ValidatedFunction类与新版validated函数可以直观看到「可调用对象 → 装饰器」的演进维度练习 6.5ValidatedFunction类练习 7.1validated装饰器函数声明方式def f(...)后f ValidatedFunction(f)validated直接修饰替换产物自定义类的实例__call__普通函数wrapper类方法支持失败self无法绑定直接可用错误聚合逐个校验、遇到第一个错误即抛聚合所有错误后一次性抛出返回校验if self.retcheck: self.retcheck.check(result)带from None的Bad return补丁新版不仅语法更优雅还修复了方法绑定问题并增强了报错的可读性——这正是练习 7.1 的全部意义。5. 进阶装饰器的暗面与后续练习预告练习 7.1 的代码量虽小但它暴露了朴素装饰器的一连串隐患下一节练习 7.2 将逐一补课元数据丢失logged包装后add.__name__变成wrapperdocstring 也丢失——需要functools.wraps(func)修复练习 7.2 的 (a)带参数的装饰器logformat({func.__code__.co_filename}:{func.__name__})这类用法需要「装饰器工厂」三层嵌套结构练习 7.2 的 (b)装饰器链与描述符冲突logged与classmethod、staticmethod、property叠加时的顺序问题练习 7.2 的 (c)校验方式的分歧enforce(xInteger, ...)用关键字参数指定类型而非注解练习 7.2 的 (d)。这也是课程设计者 David Beazley 在练习 7.2 末尾给出的提醒Writing robust decorators is often a lot harder than it looks写出健壮的装饰器往往比看起来难得多。6. 动手清单与自检按照课程惯例练习应在仓库根目录python-mastery进行参考解法位于 Solutions/7_1/ 目录包含logcall.py、sample.py、validate.py三个成品文件。你可以对照以下清单自检logcall.py中的logged能打印 Adding logging to ...导入时与 Calling ...调用时sample.py导入后add(3,4)返回7且先打印 Calling addvalidate.py中的validated支持validated注解写法add(2, 3)抛出聚合两条错误的TypeError: Bad Argumentspow(2, -1)抛出TypeError: Bad return: Expected class int且无异常链级联validated修饰的sell方法在Stock类中可正常调用nshares传负数时被PositiveInteger拦截。完成这些验证你就掌握了「外层接收函数、内层wrapper转发、返回wrapper完成替换」这一装饰器的全部核心套路可以信心满满地进入练习 7.2 的装饰器进阶之旅。本文基于 Exercises/ex7_1.md 编写官方解法见 Exercises/soln7_1.md完整可运行源码见 Solutions/7_1/validate.py、Solutions/7_1/logcall.py 与 Solutions/7_1/sample.py。课程练习总览见 Exercises/index.md。赞分享示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载相关推荐猫抓浏览器资源嗅探插件把网页视频图片存到本地的新手指南猫抓浏览器资源嗅探插件把网页视频图片存到本地的新手指南 把猫抓cat catch想象成站在浏览器和网页之间的雷达页面加载过的视频、音频、图片全都要示例工程教程Python Mastery 实战装饰器进阶——元数据复制、带参装饰器与校验封装Python Mastery 实战装饰器进阶——元数据复制、带参装饰器与校验封装 导读 本篇技术指南围绕 Exercises/soln7_2.md https示例工程教程装饰器支持pybind11 Python装饰器集成装饰器支持pybind11 Python装饰器集成 概述 在现代Python开发中装饰器Decorator是一种强大的元编程工具它允许开发者在不修改原开发工具上一篇HIXL 传输路径总览三引擎识别、日志定位与性能统计解读下一篇Golden Grid System实战案例如何用18列网格构建自适应博客页面创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进