在正式展开教程之前,我们先来回答两个基础但关键的问题:
- 什么是 Python 缓存(cache)和 CLI 工具(命令行工具)?
- 为什么它们很重要?
缓存(Cache)与命令行工具(CLI)#
Python 里,缓存(Cache)是什么?#
缓存是一种性能优化机制,它通过存储计算结果来避免重复执行耗时的操作。
一想到缓存,可能很多人想到的是网络开发里面的 HTTP 缓存,但是缓存其实还有很多的用途。比如,在 Python 中,尤其是当你编写递归函数、慢速计算或网络请求等重复调用的函数时,缓存能让你的程序快得惊人。常用的工具就是 functools.lru_cache
装饰器,它能自动保存你之前计算过的结果。
为什么缓存这么重要?
- 减少函数重复计算,提升速度
- 节省资源(CPU、内存等)
- 这是开发高性能应用程序的关键技巧
在 Python 中,缓存通常是通过装饰器来实现的。最常用的就是 functools.lru_cache
,它可以自动缓存函数调用的结果。之后的小结里,我们就会详细介绍这个装饰器。
CLI 工具(命令行工具)是什么?#
CLI 是 Command Line Interface 的缩写,中文是“命令行界面”。CLI 工具指的是可以在终端(命令行)中运行的程序,用户通过输入指令与程序交互。
你写的 Python 脚本,本质上就是一个 CLI 工具。当你运行:
python your_script.py
你就是在用命令行运行程序。
为什么重要?
- CLI 工具可以批量处理任务,无需图形界面
- 更方便部署、调试与自动化
- 是数据工程、系统管理、脚本开发等工作的基础技能
掌握缓存 + CLI 工具,你就拥有了加速程序、提升效率的一把“组合钥匙”。
什么是装饰器?#
装饰器(Decorator)是 Python 中的一个语法糖,允许你在函数或类定义时进行额外的功能“包装”。
最简单的形式就是用 @装饰器名
放在函数定义的上一行。例如:
def my_decorator(func): def wrapper(*args, **kwargs): print("Before call") result = func(*args, **kwargs) print("After call") return result return wrapper
@my_decoratordef say_hello(): print("Hello!")
say_hello()
输出:
Before callHello!After call
这段代码做了什么?首先:
def my_decorator(func): def wrapper(*args, **kwargs): print("Before call") result = func(*args, **kwargs) print("After call") return result return wrapper
这是一个装饰器函数,它接受一个函数 func
作为参数,并返回一个新的函数 wrapper
。
装饰器的作用:
- 在调用原始函数 之前 做一些事(这里是打印 “Before call”)
- 调用原始函数本身(
func(*args, **kwargs)
) - 在调用之后再做一些事(这里是打印 “After call”)
接下来这段代码:
@my_decoratordef say_hello(): print("Hello!")
这就是使用装饰器的方式。
等价于:
def say_hello(): print("Hello!")
say_hello = my_decorator(say_hello)
最终效果:
当你执行 say_hello()
,实际上运行的是 wrapper()
这个函数,所以打印的顺序是:
Before callHello!After call
也就是说,这个例子展示了装饰器的本质:用一个函数(wrapper)包装另一个函数(say_hello)来扩展它的功能,而无需修改原函数的代码。
但是,为什么 my_decorator
里面还要定义一个 wrapper
函数?不能直接写一个装饰器函数吗?#
简短回答是:
可以直接写 wrapper 函数,但如果你想写通用的装饰器(能装饰任意函数、带参数、复用),那就必须用一个外层函数包住一个内层的
wrapper
。
让我们通过几个例子说明这个“结构”的必要性。
我们能不能只写一个 wrapper
装饰函数?#
可以,但它只能装饰一个特定的函数。
比如这样:
def wrapper(): print("Before") print("Hello!") print("After")
wrapper()
这个 wrapper()
就是个普通函数,根本没“装饰”谁。
你也可以“硬包装”一个固定函数:
def wrapper(): print("Before") say_hello() print("After")
这种写法只适用于 say_hello()
,不具备通用性,也不能用 @装饰器名
的形式来装饰其他函数。
真正通用的装饰器需要两层函数结构#
我们再来看一遍标准装饰器写法:
def my_decorator(func): # <--- 外层,接受要装饰的函数 def wrapper(*args, **kwargs): # <--- 内层,包装逻辑 print("Before") result = func(*args, **kwargs) print("After") return result return wrapper # 返回包装后的函数
这两层结构的作用:
层级 | 作用 |
---|---|
my_decorator | 接收原始函数作为参数 |
wrapper | 定义新的行为(前后加东西),并调用原函数 |
return wrapper | 返回这个“增强版函数”给 Python 使用(替换原函数) |
这样你才能写出:
@my_decoratordef say_hello(): print("Hello!")
Python 在解释 @my_decorator
的时候,实际上做了:
say_hello = my_decorator(say_hello)
类比一下,更容易理解,你可以把这个结构想象成做饭时用保鲜膜包住便当:
- 外层函数:选择你要包的便当(哪个函数)
- 内层
wrapper
:负责包东西(加打印、加缓存等逻辑) - 返回
wrapper
:告诉别人,“吃这个已经包好的饭”
如果你真的只想装饰一个固定函数,确实可以不嵌套#
比如你不需要装饰通用函数,只想给 say_hello()
加点东西,那可以这样:
def say_hello(): print("Hello!")
def say_hello_with_log(): print("Before") say_hello() print("After")
say_hello_with_log()
这没错,但不能用 @语法糖
,也不能通用。
内层
wrapper()
是真正加逻辑的地方,外层函数my_decorator()
是为了让这个装饰器通用、动态、可复用。两层结构是“装饰器”的基本语法模式。
装饰器是 Python 中一种非常强大的语法机制,常用于:
- 日志记录
- 计时 / 性能分析
- 缓存(比如
@lru_cache
) - 权限检查
- Web 路由(如 Flask 中的
@app.route
)
那为什么要用装饰器来“包装”函数?#
1. 不修改原函数代码,就能扩展功能#
假设你有一个已经写好的函数(比如别人写的库函数、或者你在多个地方用过的函数),你现在想:
- 在它执行前打印日志
- 或者在它执行后记录耗时
- 或者添加缓存
- 或者做权限检查
如果你直接在原函数里加逻辑,会:
- 破坏函数的原始结构
- 增加耦合度(每次都要修改函数本身)
- 不容易复用或统一控制
使用装饰器,你就可以做到不动老代码,功能照样加上去。
2. 装饰器可以复用,多个函数一起增强#
比如你写了一个日志装饰器:
def log_decorator(func): def wrapper(*args, **kwargs): print(f"[LOG] Calling {func.__name__}") return func(*args, **kwargs) return wrapper
你可以同时加在很多函数上:
@log_decoratordef login(): ...
@log_decoratordef logout(): ...
统一加功能,非常方便。
3. 符合“开放封闭原则”(OCP)#
这是软件设计中的一个经典原则:“对扩展开放,对修改封闭。”
装饰器就完美地做到了:你可以扩展函数行为,但不修改它的源代码。
那么,什么是“开放封闭原则”(Open-Closed Principle, OCP)?
这是面向对象设计(OOP)中的五大原则(SOLID)之一:
“对扩展开放”: 你可以添加新功能或逻辑
“对修改封闭”:但你不应该动原来的代码
为什么要这样设计?因为:
- 原代码可能很复杂/已经上线/依赖方很多,不敢轻易动
- 修改原函数容易引入新 bug
- 改动越小,风险越小,测试成本越低
所以我们希望: 我能增加功能,但又不碰老代码。而这,正是装饰器的价值所在。
举个例子:没用装饰器时
def send_email(): print("Sending email...")
现在你要加一个功能:记录日志。
一种方法是直接改原函数:
def send_email(): print("[LOG] Calling send_email") print("Sending email...")
这就违反了 OCP,因为你修改了函数内部。
用装饰器来扩展功能(符合 OCP)
def log_decorator(func): def wrapper(*args, **kwargs): print(f"[LOG] Calling {func.__name__}") return func(*args, **kwargs) return wrapper
@log_decoratordef send_email(): print("Sending email...")
这里你就:
- 没动
send_email()
的原始定义 - 用装饰器“包装”了它,扩展了功能
- 增强了功能,却保持了代码封闭
这就是对“扩展开放、对修改封闭”的经典体现。
再举一个更复杂的例子
比如你有一个函数是机器学习模型预测接口:
def predict(data): return model.predict(data)
现在你想加:
- 打印请求时间
- 记录到日志系统
- 缓存结果
不用装饰器时,你可能要这样改:
def predict(data): start = time.time() print("Predicting...") result = model.predict(data) print("Time taken:", time.time() - start) return result
很快就一团糟,而且改了原函数。
用装饰器就可以优雅拆分:
def timing(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f"[Time] {func.__name__} took {time.time() - start:.4f}s") return result return wrapper
@timingdef predict(data): return model.predict(data)
这就扩展了功能(加时间统计),又封闭了原函数定义,干净漂亮。
总结这个小点:
装饰器让你在不动原函数的情况下,灵活加功能 。这就是“开放封闭原则”的体现。
4. Python 本身大量使用装饰器机制#
如果你用过:
@classmethod
/@staticmethod
@property
@app.route('/path')
(Flask)@lru_cache
(标准库)@pytest.mark.parametrize
(pytest)
这些全都是装饰器。掌握它,等于掌握了 Python 中很多高级用法。
打个比喻:
装饰器就像给电器插上一个“智能插座”:
- 插座可以控制电源(扩展功能)
- 你不用拆开电视或冰箱(不改原函数)
- 同一个插座可以换着用在多个设备上(复用)
如果你理解了装饰器,接下来看 @lru_cache
就会感觉轻松多了,因为它的本质就是一个预定义好的装饰器。
课后作业练习(先自己做,再对后面的答案)#
请完成以下练习题,检查你是否理解了装饰器的结构与作用。
选择题#
-
下列关于装饰器的说法中,错误的是? A. 装饰器可以扩展函数功能但不修改原函数 B. 装饰器语法是 Python 的语法糖 C. 装饰器无法作用于带参数的函数 D. 装饰器可以复用到多个函数上
-
@functools.lru_cache()
是什么作用? A. 检查函数参数类型 B. 缓存函数返回值,避免重复计算 C. 限制函数调用次数 D. 实现递归算法
判断题#
- 装饰器只能用于函数,不能用于类。(True / False)
- 使用装饰器的最大好处是代码运行更快。(True / False)
- 使用
@装饰器名
等价于:函数名 = 装饰器名(函数名)
。(True / False)
编程题#
6. 编写一个装饰器 log_args
,能在函数执行前,打印该函数的所有参数值。#
要求输出格式为:
[ARGS] x=1, y=2
并使用它装饰一个函数 add(x, y)
,返回两数之和。
7. 请你写一个装饰器 timer
,用于计算函数运行时间。#
要求:
-
在函数开始前记录时间
-
在函数执行后打印耗时信息,例如:
[TIMER] func_name took 0.0023s -
使用
time
模块来计时 -
用它装饰一个
slow_add(x, y)
函数,该函数用time.sleep(1)
模拟耗时
标准答案#
选择题答案#
- C(装饰器是可以用于带参数函数的,只需要使用
*args, **kwargs
) - B
判断题答案#
- False(装饰器也可以用于类)
- False(装饰器的最大优势是 功能增强而不修改原代码,不是加速本身)
- True
编程题答案#
第6题:#
def log_args(func): def wrapper(*args, **kwargs): args_repr = ", ".join(f"{arg}" for arg in args) kwargs_repr = ", ".join(f"{k}={v}" for k, v in kwargs.items()) all_args = ", ".join(filter(None, [args_repr, kwargs_repr])) print(f"[ARGS] {all_args}") return func(*args, **kwargs) return wrapper
@log_argsdef add(x, y): return x + y
print(add(1, 2))
输出示例:
[ARGS] 1, 23
第7题:#
import time
def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) duration = time.time() - start print(f"[TIMER] {func.__name__} took {duration:.4f}s") return result return wrapper
@timerdef slow_add(x, y): time.sleep(1) # 模拟耗时操作 return x + y
print(slow_add(3, 4))
输出示例:
[TIMER] slow_add took 1.0001s7
如果你准备好了,那就让我们进入到下一篇教程。