ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python装饰器详解:从闭包原理到日志鉴权重试实战

Python装饰器详解:从闭包原理到日志鉴权重试实战 你有没有经历过这样的场景项目里已经躺了三十多个函数产品经理突然跑过来让你给每个接口加上调用日志、耗时统计、权限校验。如果你还在一处处复制粘贴print(fxxx called)那这篇文章正好是为你准备的。Python 里的装饰器我习惯叫它代码的优雅整容术与逻辑劫持。说它整容是因为它能在不改动原函数内部代码的前提下给函数塑形——加上日志、鉴权、缓存、重试这些外壳说它劫持是因为它在调用函数之前和之后悄悄插入你的逻辑甚至可以决定原函数到底跑不跑。无论你是刚学完 Python 基础语法、想把函数玩出花样的新手还是已经写了不少业务代码、正在为重复劳动发愁的开发者理解装饰器的机制都能让你从会写函数进阶到会包装函数代码复用性和维护性完全上一个台阶。这篇内容不止讲装饰器的语法我会把闭包的底层原理、带参装饰器的嵌套结构、实际项目里高频场景的完整代码以及我踩过的坑全部拆开揉碎讲一遍。1. 从重复代码说起装饰器到底在替你做什么1.1 一个真实的改造现场三十个接口函数让你加日志假设你正在做一个简单的订单系统目前有三个核心函数登录、查用户信息、创建订单。它们的原始实现大致长这样def login(username, password): # 几十行业务逻辑 return {code: 0, msg: login success} def get_user(user_id): # 查询数据库等逻辑 return {nickname: 张三, level: 3} def create_order(user_id, amount): # 校验库存、生成订单号 return {order_id: 20240901-001, amount: amount}现在问题来了老板要求每个接口都要记录谁在什么时候调用了它、参数是什么、耗时多久、返回了什么。没有装饰器的时候你最直接的想法是在每个函数里加重复代码import time def login(username, password): start time.time() print(f[LOG] 调用 login, 参数: {username}, {password}) result {code: 0, msg: login success} print(f[LOG] login 返回: {result}, 耗时: {time.time() - start:.4f}s) return result一个函数改起来还好三十个函数呢你会被迫复制粘贴大量的埋点代码。而且这种做法的弊端显而易见侵入式修改业务代码和日志逻辑混在一起读起来难受出了问题也很难排查。容易漏改函数一多总有几个忘记加日志线上问题就很难追踪。修改成本高哪天想统一把日志格式换掉又得把所有函数翻一遍。这时候装饰器就是最自然的解法。它的核心思路是日志逻辑不该写在业务函数身体里而应该包在业务函数外面。1.2 装饰器的核心转变把逻辑从身体里抽到壳子上用装饰器改造后业务函数本身完全不用动只需要在定义处加一行log_decorator def login(username, password): # 原始业务逻辑一个字符都不改 return {code: 0, msg: login success} log_decorator def get_user(user_id): return {nickname: 张三, level: 3} log_decorator def create_order(user_id, amount): return {order_id: 20240901-001, amount: amount}login、get_user、create_order这三个名字现在指向的不再是原始函数本体而是一个被装饰器包装过的新函数。当代码调用login(...)时真正执行的其实是包装壳里的逻辑先记录参数、再调用原始函数、然后记录返回值和耗时。打个比方原始函数是一个旅客装饰器是机场的安检通道。旅客不需要在自己身上装安检设备只需要从通道里走一趟安检流程就自动完成。你后续想加液体检查、加防爆检查只需要修改安检通道装饰器不需要把每个旅客函数重新改造一遍。1.3 整容术和逻辑劫持到底指什么优雅整容术很好理解不动内部器官只改变外表。业务函数内部的逻辑是内脏装饰器在外面做美化或增强。加日志、加耗时统计、加权限验证本质上都是在表皮层动刀。逻辑劫持则更进一步装饰器不只是在前后加点东西它可以完全接管函数的执行流程。原函数要不要执行、什么时候执行、执行结果怎么处理都由装饰器说了算。一个最典型的情况就是权限校验def admin_required(func): def wrapper(user, *args, **kwargs): if user.role ! admin: # 到这里就返回了原函数根本不会被调用 return {code: 403, msg: no permission} return func(user, *args, **kwargs) return wrapper用户没有管理员权限时包装函数直接返回 403原始函数压根没机会执行。这就是劫持——装饰器把原本属于原函数的执行权夺了过来然后根据自定义规则决定是否放行。这个思想在后面的鉴权、缓存、限流、重试场景中会反复出现。2. 撕开装饰器的包装纸闭包、函数对象与 语法糖2.1 Python 里函数是一等公民先接受这个事实要理解装饰器第一步不是学语法而是接受一个基本事实在 Python 里函数也是对象。它跟整数、字符串、列表一样可以被赋值给变量可以作为参数传进另一个函数也可以作为返回值从函数里蹦出来。def say_hello(): return hello # 函数也是对象可以直接赋值给别的变量 greet say_hello print(greet()) # hello # 函数可以作为参数传入另一个函数 def call_func(f): return f() , python print(call_func(say_hello)) # hello, python很多刚学 Python 的同学看到这里会有点不适应明明我在函数名后面没加括号它怎么就变成了一个可调用的变量没错不加括号的函数名代表的是这个函数对象本身加了括号它才去执行。装饰器利用的恰恰是不加括号的行为——函数对象可以被传来传去。2.2 闭包是装饰器的地基装饰器的底层技术是闭包。闭包简单说就是内层函数记住了外层函数的局部变量即使外层函数已经执行完毕这些变量依然存活并陪伴内层函数走完一生。看一个最小闭包示例def outer(message): def inner(): print(f收到消息: {message}) return inner func outer(hello) # 外层函数已经执行完了但 inner 依然记得 message 的值 func() # 收到消息: hello你可以把inner想象成一个背着背包的旅行者。背包里装着外层函数运行时的局部变量message。哪怕outer函数已经返回旅行者背着背包继续走随时能从背包里取出message来用。装饰器就是外层函数负责收函数内层函数负责干活的组合。外层函数接收一个函数作为参数内层函数记录、调用这个函数并把调用结果返回出去。这个结构在装饰器里被无限复用。2.3 语法糖到底做了什么只是一个简写。下面两种写法是完全等价的# 写法一不用 def login(username, password): return {code: 0, msg: success} login log_decorator(login) # 手动把原函数交给装饰器再用返回值覆盖原变量 # 写法二用 log_decorator def login(username, password): return {code: 0, msg: success}log_decorator这行的意思是定义完login函数后立即执行log_decorator(login)然后把返回结果重新赋值给login这个名字。装饰器在函数定义的那一刻就会执行而不是在函数被调用时才执行。这点特别容易被人忽略后面讲坑的时候还要提。提示装饰器接收的是一个函数对象返回的通常也是一个函数对象。如果在装饰器内部忘记返回包装函数原函数名就会被覆盖成None调用时直接报TypeError: NoneType object is not callable。2.4 第一个完整装饰器的推演过程现在手动实现一个timer_decorator用来统计被装饰函数的执行耗时。import time def timer_decorator(func): # 包装函数接收任意参数 def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) # 调用原始函数 cost time.time() - start print(f[TIMER] {func.__name__} 耗时 {cost:.4f}s) return result # 把原函数返回值原样传出去 return wrapper为什么内层函数必须写成*args, **kwargs因为装饰器不知道将来会装饰什么函数。有的函数接收两个位置参数有的接收关键字参数有的什么都不接收。用*args和**kwargs把参数原封不动地接住再原封不动地传给原始函数这样装饰器才有通用性。还有个细节内层函数拿到result之后必须return出去。很多人第一次写装饰器容易漏掉这个return结果发现被装饰的函数输出变成了None。原因就是你调用的是wrapper而wrapper没有把原函数的结果交还给你。实际使用timer_decorator def heavy_compute(n): total 0 for i in range(n): total i return total print(heavy_compute(1000000))输出类似[TIMER] heavy_compute 耗时 0.0312s 499999500000耗时统计是先打印的返回值是后返回的顺序很清晰先记录开始时间、执行原函数、算耗时打印、把原结果返回给上层调用者。3. 实战拆解日志、鉴权、重试三个高频装饰器3.1 日志装饰器参数、返回值、耗时全记录日志装饰器是装饰器里最万能的应用几乎每个项目都有。我自己写的日志装饰器通常至少包含四个信息函数名、调用参数、返回值、耗时。异常情况也要单独记录不能因为日志把真实异常吞掉。import time import traceback def log_decorator(log_funcprint): def decorator(func): def wrapper(*args, **kwargs): start time.time() args_repr [repr(a) for a in args] kwargs_repr [f{k}{v!r} for k, v in kwargs.items()] signature , .join(args_repr kwargs_repr) log_func(f[LOG] 调用 {func.__name__}({signature})) try: result func(*args, **kwargs) elapsed time.time() - start log_func(f[LOG] {func.__name__} 返回 {result!r}, 耗时 {elapsed:.4f}s) return result except Exception as e: log_func(f[LOG] {func.__name__} 异常: {e}) log_func(traceback.format_exc()) raise # 异常一定要重新抛出不能替调用方做决定 return wrapper return decorator这里用了带参数的装饰器log_decorator(log_funcprint)后面会详细解释这种三层嵌套的结构现在先知道它能灵活指定日志输出到哪里就行。注意!r这个格式化符号它能把参数以repr形式输出字符串会带上引号这样日志里参数类型一目了然。在实际项目中你只要把log_func换成项目自己的日志对象比如logging.getLogger(app).info整组业务函数就能统一接入标准日志系统比在每个函数里手写 print 可维护得多。3.2 鉴权装饰器逻辑劫持的典型战场鉴权是我觉得最能体现劫持二字的装饰器场景。它可以拦截未登录的请求、校验用户角色、判断接口权限并且在权限不足时不让原函数执行分毫。假设你在写一个 Web 后端不依赖任何框架模拟逻辑接口函数需要从请求上下文里取当前用户class User: def __init__(self, user_id, role): self.user_id user_id self.role role def require_login(func): def wrapper(*args, **kwargs): # 从 kwargs 里找当前用户开发时经常这么传方便测试 user kwargs.get(current_user) if user is None: return {code: 401, msg: 请先登录} return func(*args, **kwargs) return wrapper def require_admin(func): def wrapper(*args, **kwargs): user kwargs.get(current_user) if user is None: return {code: 401, msg: 请先登录} if user.role ! admin: return {code: 403, msg: 无权访问} return func(*args, **kwargs) return wrapper然后分别用于不同接口require_login def get_my_profile(current_userNone): return {profile: f用户 {current_user.user_id} 的资料} require_admin def delete_order(order_id, current_userNone): # 只有管理员能走到这里 return {code: 0, msg: f订单 {order_id} 已删除}测试一下bob User(user_id1, roleuser) admin User(user_id2, roleadmin) print(get_my_profile(current_userbob)) # {profile: 用户 1 的资料} print(delete_order(order_id100, current_userbob)) # {code: 403, msg: 无权访问} print(delete_order(order_id100, current_useradmin)) # {code: 0, msg: 订单 100 已删除}注意看当普通用户尝试删除订单时delete_order的业务逻辑完全没执行返回的就是装饰器给的 403。这就是逻辑劫持的极致表现装饰器握着通向原函数的闸门它不放行原函数连启动的机会都没有。3.3 重试装饰器失败后自动重试的那点经验在调用第三方 API、连数据库、发网络请求这类场景里经常遇到偶发失败。直接抛异常给用户不友好全部手动重试又太繁琐。重试装饰器可以统一解决但有几个细节我得先说清楚重试次数要能配置不能写死。重试间隔要有否则失败时瞬间空转CPU 白烧。不是所有异常都值得重试得支持指定捕获哪些异常。重试耗尽后最后一次异常要原样抛出不能让调用方以为成功了。import time def retry(max_times3, delay0.5, exceptions(Exception,)): def decorator(func): def wrapper(*args, **kwargs): last_exc None for attempt in range(1, max_times 1): try: return func(*args, **kwargs) except exceptions as e: last_exc e if attempt max_times: break time.sleep(delay) raise last_exc return wrapper return decorator使用示例retry(max_times4, delay0.2, exceptions(ConnectionError, TimeoutError)) def fetch_remote_data(): # 模拟偶发失败 import random if random.random() 0.5: raise TimeoutError(模拟超时) return {data: ok}业务调用方完全无感知失败后自动重试。这类装饰器在高并发服务里几乎是标配。再进一步生产环境通常会给delay加一点随机抖动避免大量请求失败后同步重试造成重试风暴把下游服务直接打挂。抖动就是在间隔基础上加一个随机值import random time.sleep(delay random.uniform(0, 0.2))从劫持的角度看重试装饰器劫持的是函数的异常出口原本函数抛个异常就结束了现在被装饰器半路接住又塞回执行流程里重来一次。3.4 三个装饰器的共同结构把上面三个装饰器放一起看会发现它们的长相高度一致区别只在于在 wrapper 里对原函数做了什么处理装饰器前置钩子调用原函数前调用原函数后置钩子调用原函数后可能完全绕过原函数日志记录参数、开始时间正常调用记录结果、耗时、异常否鉴权校验身份和权限校验通过才调用无是权限不足直接返回重试无循环尝试捕获异常、决定是否重试否但会反复调用看完这个表格你应该能感受到装饰器本质是一个模板核心工作就是在调用原函数这条主线上选择性地插入额外逻辑。你只需要把注意力放在三个位置调用前、调用本身、调用后。4. 进阶形态带参装饰器与类装饰器4.1 为什么需要带参装饰器上面的retry装饰器已经出现了带参数的形式retry(max_times4, delay0.2, exceptions(TimeoutError,)) def fetch_remote_data(): ...如果retry只是普通单层装饰器retry后面就只能是retry(func)这种形式无法额外配置参数。要支持retry(参数)装饰器本身必须再包一层最外层接收配置参数中间层接收函数内层才是真正的包装逻辑。整个结构像这样def retry(max_times3, delay0.5, exceptions(Exception,)): # 这一层接收配置参数 def decorator(func): # 这一层接收被装饰函数 def wrapper(*args, **kwargs): # 这一层接收调用时的参数 for attempt in range(1, max_times 1): try: return func(*args, **kwargs) except exceptions: if attempt max_times: raise time.sleep(delay) return wrapper return decorator调用retry(max_times3)时Python 先把retry(max_times3)执行掉得到一个decorator然后把这个decorator应用到函数上即decorator(func)。初学装饰器时我经常被这三层嵌套绕晕。记忆方法是最外层不碰函数只碰配置碰函数的是中间层碰调用参数的只能是内层。一个参数被用到哪一层它就该出现在哪个作用域顺着这条线走嵌套就不会乱。4.2 把装饰器写成类更直观的状态存储函数式装饰器最大的短板是没办法优雅地保存跨调用的状态。如果你想知道某个函数被调用了多少次、累计耗时多少函数式写法也能实现但得靠外层闭包里的可变变量来记录读起来别扭。更直观的方式是用类实现装饰器。一个类的实例可以保存属性天然适合存状态。实现的关键是__call__魔法方法它让类的实例可以像函数一样被调用。import time class CallStats: def __init__(self, func): self.func func self.call_count 0 self.total_time 0.0 def __call__(self, *args, **kwargs): self.call_count 1 start time.time() result self.func(*args, **kwargs) self.total_time time.time() - start return result def get_stats(self): return { call_count: self.call_count, total_time: round(self.total_time, 4), avg_time: round(self.total_time / self.call_count, 4) if self.call_count else 0 }使用CallStats def process(item): return item * 2 for i in range(10): process(i) print(process.get_stats()) # {call_count: 10, total_time: ..., avg_time: ...}这里CallStats做的事情是用CallStats(process)创建了一个实例这个实例通过__call__方法变成了可调用对象并覆盖了process这个名字。因为实例的call_count和total_time是实打实的对象属性所以统计状态被安全地保存在对象里不会像函数闭包变量那样显得藏得深。4.3 类装饰器与函数装饰器的选择标准用哪一种不是谁替代谁的关系我按几年使用经验给个选择标准情况推荐原因只做横切逻辑无跨调用状态函数式装饰器结构简单、可读性好需要在多次调用间记录状态类装饰器属性存储更直观自带可调用的get_stats方法逻辑复杂、包含多个辅助方法类装饰器可以拆分成私有方法避免闭包里堆满逻辑需要动态修改装饰器参数函数式带参装饰器三层嵌套天然支持配置参数另外提一句类装饰器在__init__里接收函数但如果你想让它也能带参配置照样可以套三层__init__接收配置参数__call__接收函数__call__内部再返回一个wrapper。套路一模一样。5. 容易踩的坑wraps、执行顺序、性能与调试5.1 functools.wraps 到底解决了什么我见过太多初学者写的装饰器不带functools.wraps结果调试时函数名全变成了wrapper对外接口文档也是一片混乱。看个反例def my_decorator(func): def wrapper(*args, **kwargs): 我是包装函数 return func(*args, **kwargs) return wrapper my_decorator def add_numbers(a, b): 计算两数之和 return a b print(add_numbers.__name__) # wrapper print(add_numbers.__doc__) # 我是包装函数add_numbers这个名字指向的其实是wrapper所以它的__name__和__doc__都变成了包装函数的值。如果框架或工具依赖函数元信息做文档生成、路由注册、序列化这种身份丢失就会出 bug。解决办法是在装饰器内部的wrapper上加一行functools.wraps(func)import functools def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper my_decorator def add_numbers(a, b): 计算两数之和 return a b print(add_numbers.__name__) # add_numbers print(add_numbers.__doc__) # 计算两数之和functools.wraps做的事情是把原函数的__name__、__doc__、__module__、__dict__等元信息复制到wrapper上并额外设置__wrapped__指向原函数。我个人的习惯是写任何自定义装饰器第一行先写functools.wraps(func)这行代码的成本几乎为零但能避免后面调试时多花半小时。5.2 多个装饰器的执行顺序从下往上包装从上往下执行叠加装饰器是很多人理解错的重灾区。看这个例子def first(func): def wrapper(): print( first 调用前) func() print( first 调用后) return wrapper def second(func): def wrapper(): print( second 调用前) func() print( second 调用后) return wrapper first second def business(): print(业务函数执行)调用business()的输出是 first 调用前 second 调用前 业务函数执行 second 调用后 first 调用后为什么会是这个顺序因为装饰器的叠加等价与嵌套调用business first(second(business))second先被应用business先变成了包着原函数的 second 版然后first再应用到这个second 版上从外到内形成first包second包原函数。所以执行时最外层first的调用前先打印接着进入second的调用前最后才到原函数本体。记忆口诀装饰包装从下往上执行脱壳从上往下。这个特性在签名校验、事务控制和权限装饰器叠加时尤其重要。比如你希望登录校验始终在最外层把它放在最下面被最后装饰反而会被包在最外面需要明确规划装饰器的上下顺序。5.3 性能与调试装饰器不是免费午餐每次调用被装饰的函数都要先执行外层包装函数的分发逻辑这比直接调用原始函数多出几次函数跳转和args/kwargs打包解包。在单个函数调用上这个开销微乎其微通常可以忽略。但在每秒几十万次调用的热点路径上装饰器的性能开销就会暴露出来。我实测过一个很薄的装饰器能带来微秒级别的额外耗时普通业务无感可如果这个函数恰好是排序算法、数值循环的核心回调函数加装饰器和不加装饰器的差距就能体现到基准测试结果里。调试时也有一套技巧。当你怀疑装饰器内的逻辑影响了结果可以用func.__wrapped__绕过所有装饰器直接调用原始函数# 如果装饰器都用了 functools.wraps这里就是原始函数 raw_result business.__wrapped__()或者直接在装饰器内部临时打印调用栈确认当前执行路径。结合functools.wraps保留的函数名用调试器比如 pdb 或 IDE 断点定位问题时你能在调用栈里看到真实的函数名而不是满屏的wrapper。5.4 三个真实翻车现场我把自己和朋友踩过的典型坑列一下提前避雷。翻车 1装饰器忘了 return函数变成 Nonedef bad_decorator(func): def wrapper(): print(干活...) func() # 漏了 return wrapper bad_decorator def hello(): print(hello) hello() # TypeError: NoneType object is not callable因为装饰器函数没有返回任何值hello这个名字被赋成了None。修复方法就是补上return wrapper。翻车 2带参装饰器少写一层# 错误写法 def retry(max_times): def wrapper(func): # 这里接收的应该是被装饰函数但少了一层 ...当你写retry(3)时Python 会执行retry(3)得回来一个decorator函数然后再用这个decorator去接收被装饰函数。如果retry(3)直接返回了wrapper内层包装函数那实际出来的是wrapper(func)而不是decorator(func)执行就乱了。检查标准很简单带参装饰器一定有三层 def少一层就报错或行为诡异。翻车 3类里直接定义装饰器函数self 问题如果你在类的方法里临时定义一个装饰器直接套在另一个方法上会因为self的传入方式引发意外class Service: def my_decorator(self, func): def wrapper(self, *args, **kwargs): return func(self, *args, **kwargs) return wrapper my_decorator # 这里 my_decorator 接收到的其实是方法对象不是 self 场景 def do_thing(self): pass这个写法很容易把self搞混。一般建议装饰器独立定义在类外面或者用staticmethod包装不要让实例方法直接充当装饰器。6. 装饰器的边界感哪些逻辑适合劫持哪些不适合6.1 适合装饰器管的横切逻辑什么样的逻辑天生适合装饰器我的判断标准有三条横切多个函数日志、鉴权、限流、缓存、重试、事务、耗时统计几乎每个函数都要做把它们从业务函数里抽出去是最大的价值。与具体业务无关装饰器本身不知道也不该知道业务函数内部怎么算它只关心调用前做什么、调用后做什么。假如一个装饰器需要判断函数返回值里的业务字段那它已经开始过度耦合。策略可以统一配置通过带参装饰器不同函数可以有不同的重试次数、不同的权限级别、不同的缓存有效期但机制是同一套。符合这三条的逻辑放进装饰器没有副作用还能让业务函数保持纯粹。我见过很舒服的设计请求处理函数只要写自己的业务返回值日志、权限、限流全部由装饰器栈完成代码清单得像待办事项表。6.2 不适合用装饰器的场景装饰器也不是万能胶。下面是几个反面场景依赖函数内部局部状态的逻辑不能用装饰器。装饰器只能看到传入的参数和返回的结果拿不到函数内部的局部变量。比如你想在函数执行到某个中间步骤时做特殊处理装饰器是无能为力的这个逻辑必须老实写在函数内部。和返回值深度耦合的业务判断不要硬塞进装饰器。比如如果订单金额超过 1000走领导审批流程这跟业务强相关一旦放进通用装饰器它就要去解析各种函数的返回结构维护成本越来越高。这种逻辑更适合留在业务函数内部或者由调用方在拿到结果后处理。装饰器叠加过多会让代码变得难以调试。如果一个接口函数头顶上盖了七八个装饰器调用链一层套一层报错时你很可能需要展开很长的栈帧才能定位到真正的业务逻辑。此时应该考虑用框架层的中间件、过滤器替代部分职责把粒度放粗。6.3 装饰器与中间件的分工定位Web 框架里的中间件比如 Django 的 Middleware、Flask 的 before_request/after_request、FastAPI 的依赖注入本质上是更大粒度的装饰器思想。它们的思路一致在不修改每个视图函数的前提下在请求进入视图之前和离开视图之后插入统一逻辑。它们的区别在于作用粒度装饰器精准控制到函数级只影响被标记的函数。中间件全局控制到请求级所有经过框架路由的请求都会被插入逻辑。实际项目的分层建议是全局性、与路由无关的逻辑优先用中间件只针对一部分接口的个性化逻辑用装饰器。比如全站访问日志、统一跨域、全局异常兜底用中间件单个接口的超时重试、某个模块的权限细粒度校验、部分接口的缓存策略用装饰器。两者配合才是完整的横切逻辑解决方案。我在项目里最常见的实践是装饰器负责函数级的横切处理中间件负责请求级的横切处理业务函数本身不放任何横切代码。这样无论是加功能还是排查问题都只需要找对应层级不会互相踩脚。最后再分享一点个人体会。我刚用装饰器的时候总想把它当成炫技工具什么逻辑都想往装饰器里塞。后来一个老同事跟我说了一句话装饰器是给代码做减法的工具不是做加法的工具。 每当你觉得可以用一个装饰器来管这件事时先检查一下它是不是真的让业务函数变轻了、让重复代码变少了如果答案是肯定的尽管用如果只是为了让代码看起来高深那可能反而是负担。如果你正在项目的多个函数里复制粘贴同样的前置逻辑或者为几十个接口的日志和权限焦头烂额装饰器就是你需要的那个整容师。从手写一个带functools.wraps的日志装饰器开始慢慢尝试鉴权、重试、限流你会发现自己对函数的包装越来越有感觉。等哪一天你不再需要查语法随手就能写出嵌套合理的装饰器时你对 Python 的掌控力就又进了一阶。
返回列表