Python-100-Days 第17天:装饰器与递归调用——函数高级应用的进阶之路

发布时间:2026/10/14 2:56:03
Python-100-Days 第17天:装饰器与递归调用——函数高级应用的进阶之路
教程文档【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/capable-ai/Python-100-Days点击查看免费下载装饰器与递归调用是 Python 函数进阶的两大核心主题前者是 Python 独有的语法糖能以声明式的方式为既有函数注入计时、日志、缓存等横切关注能力实现增强而不侵入后者则让函数可以调用自身将阶乘、斐波那契数列、回溯搜索等递归定义的问题用代码直接表达。本文以《Python-100-Days》第 17 天教程为骨架结合仓库中的源码实例如 Day31-35/code/example09.py 的参数化装饰器、Day31-35/code/example10.py 的单例装饰器完整讲解装饰器的实现原理、语法糖、functools.wraps的取消机制以及递归的收敛条件、栈溢出风险与lru_cache性能优化读完后你将能够独立编写可复用的装饰器并安全、高效地使用递归解决问题。装饰器用一个函数装饰另一个函数在函数使用进阶中我们已经了解到Python 中的函数是一等函数函数可以赋值给变量可以作为参数传入也可以作为返回值返回把一个函数作为其他函数的参数或返回值的用法称为高阶函数。装饰器正是基于高阶函数的能力实现的——装饰器本身是一个函数它的参数是被装饰的函数它的返回值是一个带有装饰功能的函数。也就是说装饰器是一个参数和返回值都是函数的高阶函数。从实际需求出发记录函数的执行时间假设有名为download和upload的两个函数分别用于模拟文件的下载和上传内部用随机休眠来模拟耗时操作import random import time def download(filename): 下载文件 print(f开始下载{filename}.) time.sleep(random.random() * 6) print(f{filename}下载完成.) def upload(filename): 上传文件 print(f开始上传{filename}.) time.sleep(random.random() * 8) print(f{filename}上传完成.) download(MySQL从删库到跑路.avi) upload(Python从入门到住院.pdf)说明上面的代码用休眠一段随机时间的方式模拟了下载和上传文件需要花费一定的时间并没有真正的联网上传下载文件。现在有一个新需求希望知道调用download和upload函数到底用了多少时间。最直接的做法是在函数开始执行时记录一个时间调用结束后再记录一个时间两个时间相减即可start time.time() download(MySQL从删库到跑路.avi) end time.time() print(f花费时间: {end - start:.2f}秒) start time.time() upload(Python从入门到住院.pdf) end time.time() print(f花费时间: {end - start:.2f}秒)问题显而易见记录时间、计算和显示执行时间的代码全部是重复代码。有编程经验的人都知道重复的代码是万恶之源。装饰器就是解决这类问题的最佳选择把与业务上传和下载无关的计时功能封装到一个函数中当upload和download需要记录时间时直接把装饰器作用到这两个函数上即可。装饰器的核心结构高阶函数 函数嵌套将记录时间的装饰器命名为record_time它的整体结构如下def record_time(func): def wrapper(*args, **kwargs): result func(*args, **kwargs) return result return wrapper理解这个结构要注意几个关键点record_time的参数func是被装饰的函数wrapper函数是带有装饰功能的函数它负责执行被装饰的函数func并返回func的返回值上面的代码在wrapper内部第 4 行和第 6 行各留了一个空行这意味着可以在这些位置添加代码来实现额外功能record_time最终返回wrapper并用wrapper替代原函数func——被装饰后调用原函数实际调用的是wrapper因此才获得了额外能力wrapper使用*args, **kwargs可变参数和关键字参数照单全收原函数的所有参数然后在调用func时原封不动地传回这样装饰器就能适配任意签名的函数Python 支持函数的嵌套定义在record_time中定义wrapper这个操作在很多编程语言中并不被支持它是闭包和装饰器能够成立的语言基础。把记录时间的功能写入装饰器import time def record_time(func): def wrapper(*args, **kwargs): # 在执行被装饰的函数之前记录开始时间 start time.time() # 执行被装饰的函数并获取返回值 result func(*args, **kwargs) # 在执行被装饰的函数之后记录结束时间 end time.time() # 计算和显示被装饰函数的执行时间 print(f{func.__name__}执行时间: {end - start:.2f}秒) # 返回被装饰函数的返回值 return result return wrapper写装饰器虽然颇费周折但这是一劳永逸的操作将来再有记录函数执行时间的需求时只需在目标函数上添加该装饰器即可。两种使用方式函数式调用与语法糖方式一直接调用装饰器函数把返回值替代原来的函数download record_time(download) upload record_time(upload) download(MySQL从删库到跑路.avi) upload(Python从入门到住院.pdf)方式二使用装饰器函数语法糖将装饰器直接放在被装饰的函数定义之上效果与方式一完全相同。所谓语法糖是指编程语言中添加的某种语法它对语言的功能没有影响但使用更加便捷、可读性更强。完整代码如下import random import time def record_time(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f{func.__name__}执行时间: {end - start:.2f}秒) return result return wrapper record_time def download(filename): print(f开始下载{filename}.) time.sleep(random.random() * 6) print(f{filename}下载完成.) record_time def upload(filename): print(f开始上传{filename}.) time.sleep(random.random() * 8) print(f{filename}上传完成.) download(MySQL从删库到跑路.avi) upload(Python从入门到住院.pdf)被装饰后的download和upload其实就是装饰器中返回的wrapper函数调用它们等同于调用wrapper因此自动获得了记录执行时间的能力。functools.wraps保留原函数以便取消装饰器如果代码的某些地方想去掉装饰器的作用、直接执行原函数定义装饰器时需要做一点点额外的工作使用 Python 标准库functools模块的wraps函数它本身也是一个装饰器放在wrapper函数上它会帮我们保留被装饰之前的函数需要取消装饰时可以通过被装饰函数的__wrapped__属性获得被装饰之前的函数。import random import time from functools import wraps def record_time(func): wraps(func) def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f{func.__name__}执行时间: {end - start:.2f}秒) return result return wrapper record_time def download(filename): print(f开始下载{filename}.) time.sleep(random.random() * 6) print(f{filename}下载完成.) record_time def upload(filename): print(f开始上传{filename}.) time.sleep(random.random() * 8) print(f{filename}上传完成.) # 调用装饰后的函数会记录执行时间 download(MySQL从删库到跑路.avi) upload(Python从入门到住院.pdf) # 取消装饰器的作用不记录执行时间 download.__wrapped__(MySQL必知必会.pdf) upload.__wrapped__(Python从新手到大师.pdf)此外wraps(func)还会把原函数func的__name__、__doc__等元信息复制到wrapper上避免装饰后函数改名换姓例如print(download.__name__)时仍显示download而不是wrapper这对调试和文档生成都很有价值。参数化装饰器从源码看输出方式的解耦装饰器函数本身也可以参数化——装饰器可以由调用者传入的参数来定制。仓库源码 Day31-35/code/example09.py 给出了一个教科书级别的参数化装饰器record它把记录耗时之后输出到哪里彻底解耦文件头部的注释点明了它的设计意图装饰器 - 装饰器中放置的通常都是横切关注cross-concern功能 所谓横切关注功能就是很多地方都会用到但跟正常业务又逻辑没有必然联系的功能 装饰器实际上是实现了设计模式中的代理模式 - AOP面向切面编程其核心实现为三层嵌套函数record→decorate→wrapperwrapper执行被装饰函数后把函数名和耗时交给外部传入的output回调处理from functools import wraps from time import time def record(output): def decorate(func): wraps(func) def wrapper(*args, **kwargs): start time() ret_value func(*args, **kwargs) output(func.__name__, time() - start) return ret_value return wrapper return decorate这样使用者可以自由组合计时逻辑与输出逻辑——源码中分别定义了输出到控制台、输出到文件、输出到 MySQL 数据库三种回调只需一行record(output_to_console)即可切换记录方式def output_to_console(fname, duration): print(%s: %.3f秒 % (fname, duration)) def output_to_file(fname, duration): with open(log.txt, a) as file_stream: file_stream.write(%s: %.3f秒\n % (fname, duration))从源码结构看这正是**面向切面编程AOP**思想的体现装饰器中放置的多是与业务逻辑无关的横切关注功能计时、日志、鉴权、缓存等通过装饰器把它们编织进业务函数既复用了代码又没有侵入业务逻辑。在Day31-35/31.Python语言进阶.md中还展示了用类定义装饰器实现__call__方法的等价写法感兴趣的读者可以对照阅读。装饰器的另一重身份装饰类单例模式装饰器的参数可以是函数也可以是类。仓库源码 Day31-35/code/example10.py 用装饰器实现了单例模式——确保一个类只能创建出唯一的对象并且给出了线程安全的版本import threading from functools import wraps def singleton(cls): 单例装饰器 instances {} lock threading.Lock() wraps(cls) def wrapper(*args, **kwargs): if cls not in instances: with lock: if cls not in instances: instances[cls] cls(*args, **kwargs) return instances[cls] return wrapper singleton class President(): def __init__(self, name, country): self.name name self.country country def __str__(self): return f{self.country}: {self.name} def main(): p1 President(特朗普, 美国) p2 President(奥巴马, 美国) print(p1 p2) # True无论创建多少次都只得到一个实例 print(p1) print(p2)这里有几个值得深挖的细节instances字典是singleton函数内的局部变量wrapper通过闭包捕获并复用它因此所有对President()的调用共享同一个实例缓存判空与创建之间用threading.Lock加锁并采用了双重检查先不加锁判一次加锁后再判一次的写法既保证线程安全又避免每次调用都加锁的性能开销由于使用了wraps(cls)同样可以通过President.__wrapped__拿到原始的类绕过单例限制直接创建新对象。President类的装饰器也是装饰器可以作用于类的明证——装饰器并不局限于函数任何可调用对象都可以作为被装饰对象。递归调用函数调用自己Python 允许函数嵌套定义也允许函数之间相互调用而且一个函数还可以直接或间接地调用自身。函数自己调用自己称为递归调用。递归的定义与两大要素收敛条件与递归公式现实中很多问题的定义本身就是递归定义。例如阶乘非负整数N的阶乘是N乘以N-1的阶乘即 $N! N \times (N-1)!$定义的左边和右边都出现了阶乘的概念因此这是一个递归定义。可以用递归调用的方式写出求阶乘的函数def fac(num): if num in (0, 1): return 1 return num * fac(num - 1)写递归函数有两个要点这一总结在仓库的公开课《算法入门系列2 - 在水一方》中同样被强调收敛条件——什么时候结束递归。上述代码第 2 行的if num in (0, 1)就是收敛条件计算到0或1的阶乘时停止递归直接返回1递归公式——每一项与前一项或前 N 项的关系。上述代码第 4 行的num * fac(num - 1)就是递归公式即阶乘的递归定义。递归与函数调用栈先进后出与栈溢出用fac(5)计算 5 的阶乘整个调用过程如下# 递归调用函数入栈 # 5 * fac(4) # 5 * (4 * fac(3)) # 5 * (4 * (3 * fac(2))) # 5 * (4 * (3 * (2 * fac(1)))) # 停止递归函数出栈 # 5 * (4 * (3 * (2 * 1))) # 5 * (4 * (3 * 2)) # 5 * (4 * 6) # 5 * 24 # 120 print(fac(5)) # 120函数调用会通过内存中称为栈stack的数据结构保存当前代码的执行现场函数调用结束后再通过栈恢复之前的执行现场。栈是先进后出的数据结构这意味着最早入栈的函数最后才返回最后入栈的函数最先返回——例如函数a调用b、b调用c则最先入栈的是a最先出栈的是c。每进入一个函数调用栈就增加一层栈帧stack frame栈帧正是保存当前代码执行现场的结构每当函数调用结束栈就减少一层栈帧。通常内存中的栈空间很小因此递归调用次数如果太多会导致栈溢出stack overflow。尝试执行fac(5000)会看到RecursionError错误错误消息为maximum recursion depth exceeded in comparison超出最大递归深度其实就是发生了栈溢出。使用官方解释器 CPython 时默认将函数调用的栈结构最大深度设置为1000 层超出即触发RecursionError。虽然可以用sys模块的setrecursionlimit函数改变递归调用的最大深度如sys.setrecursionlimit(10000)该用法可见于仓库公开课《算法入门系列2 - 在水一方》但不建议这样做——让递归快速收敛才是正确方向否则就应该考虑使用循环递推而不是递归。斐波那契数列递归写法与性能陷阱斐波那契数列前两个数都是1从第三个数开始每个数是前两个数相加的和记为f(n) f(n - 1) f(n - 2)又是一个递归定义可以写出如下递归函数def fib1(n): if n in (1, 2): return 1 return fib1(n - 1) fib1(n - 2) for i in range(1, 21): print(fib1(i))需要提醒的是这段代码虽然看起来简单明了但执行性能非常糟糕把range的第二个参数修改为51输出前 50 个斐波那契数时耗时将显著增加。原因在于递归展开时存在大量重复计算——fib1(n)会分别递归计算fib1(n-1)和fib1(n-2)而这两棵递归树之间存在海量的重叠子问题时间复杂度呈指数级增长在公开课中对此的评价是时间复杂度是几何级数级的。显然直接用循环递推是更好的选择def fib2(n): a, b 0, 1 for _ in range(n): a, b b, a b return alru_cache用缓存装饰器飞一般地优化递归除此以外还可以使用functools模块的lru_cache函数优化上面的递归代码。lru_cache本身是一个带参数的装饰器函数将其置于fib1之上它会缓存该函数的执行结果从而避免递归调用过程中产生的大量重复运算执行性能因此有飞一般的提升from functools import lru_cache lru_cache() def fib1(n): if n in (1, 2): return 1 return fib1(n - 1) fib1(n - 2) for i in range(1, 51): print(i, fib1(i))提示lru_cache函数是一个带参数的装饰器所以使用装饰器语法糖时lru_cache后面要跟上圆括号。lru_cache有一个非常重要的参数maxsize用来定义缓存空间的大小默认值是128。这个优化思路在仓库中被反复使用公开课第05次公开课-算法入门系列1-周而复始/code/example04.py 和年薪50W的Python程序员如何写代码/code/Python/opencourse/part01/example07.py 中都用lru_cache()装饰fib函数后者甚至直接输出了前 120 个斐波那契数性能依然可接受。公开课中还用「默认参数缓存」的等价技巧优化过递归其核心思想与lru_cache一致用缓存消灭重复子问题的计算。顺带一提递归在仓库中还有更广阔的应用场景——公开课《算法入门系列2 - 在水一方》中的迷宫寻路、八皇后、骑士巡逻等回溯算法都以递归为基本手段回溯法本质上是暴力搜索法的一种可看作带悔棋的递归搜索。总结装饰器是 Python 语言中的特色语法本质是一个参数和返回值都是函数的高阶函数。通过装饰器可以增强现有的函数记录执行时间、输出日志、实现单例、缓存结果等这是一种非常有用的编程技巧。装饰器内放置的多是与业务无关的横切关注功能体现了面向切面编程AOP和代理模式的设计思想结合functools.wraps可以保留原函数并通过__wrapped__随时取消装饰器的作用结合参数化设计则可以进一步解耦装饰器内部的可变行为。函数递归调用可以在代码层面将一些复杂的问题简单化但必须注意收敛条件和递归公式找到递归公式才有机会使用递归调用收敛条件则确保递归能停下来。函数调用通过内存中的栈空间保存和恢复执行现场栈空间通常很小递归如果不能迅速收敛很可能引发栈溢出错误导致程序崩溃。遇到指数级复杂度的递归时优先考虑循环递推或用lru_cache等缓存机制消除重复子问题计算。赞分享教程文档【免费下载链接】Python-100-DaysPython - 100天从新手到大师项目地址https://gitcode.com/capable-ai/Python-100-Days点击查看免费下载相关推荐Waking-Up Python进阶装饰器应用场景Waking Up Python进阶装饰器应用场景 你是否还在为重复编写日志记录、性能统计代码而烦恼是否遇到过需要临时给函数添加功能又不想修改原代码的情况知识库文档Python-100-Days 数据库进阶MySQL 视图、函数与存储过程实战指南Python 100 Days 数据库进阶MySQL 视图、函数与存储过程实战指南 本篇技术指南以 Python 100 Days 项目 Day36 45 阶文档教程Eclipse Zenoh核心功能详解发布订阅与查询回复的完美结合Eclipse Zenoh核心功能详解发布订阅与查询回复的完美结合 Eclipse Zenoh 是一款强大的数据处理平台它巧妙地融合了传统的发布/订阅模式与通信消息路由上一篇终极指南3分钟完成GitHub下载加速10倍提升下一篇如何把闲置电视盒子改造成Linux服务器一条可回退的完整Armbian刷机路线创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考