Python 从入门到精通
第 17 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力正在阅读 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
36
综合项目:从需求到上线
这一篇讲 Python 里最”魔法”的语法之一:装饰器(decorator)。你可能已经见过 @staticmethod、@property 这种带 @ 的写法。装饰器的本质是:在不改函数代码的前提下,给函数加功能。日志、计时、权限检查、缓存——都是它的经典应用。
先理解:函数是一等公民
Python 里函数也是对象,可以传来传去、可以嵌套定义:
def greet(name):
print(f"你好,{name}")
f = greet # 把函数赋给变量(不加括号!)
f("南山") # 你好,南山
def make_adder(x):
def adder(y): # 嵌套函数
return x + y
return adder # 返回函数
add5 = make_adder(5)
print(add5(3)) # 8(add5 记住了 x=5,这就是闭包)
嵌套函数”记住”了外层变量(x=5),这种结构叫闭包(closure)。装饰器就是建立在闭包之上的。
手写第一个装饰器
需求:给函数加计时功能,但不改函数本身。
import time
def timed(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # 调用原函数
elapsed = time.time() - start
print(f"{func.__name__} 耗时 {elapsed:.4f} 秒")
return result
return wrapper
def slow_add(a, b):
time.sleep(0.5)
return a + b
# 手动用法
slow_add = timed(slow_add)
print(slow_add(1, 2)) # slow_add 耗时 0.5001 秒 / 3
timed 接收一个函数,返回一个新函数 wrapper。wrapper 里:先计时 → 调原函数 → 打印耗时 → 返回结果。原函数一行没改,功能加上了。
@ 语法糖
上面的手动写法太啰嗦,Python 提供了 @ 语法:
@timed
def slow_add(a, b):
time.sleep(0.5)
return a + b
# 等价于 slow_add = timed(slow_add)
@timed 写在函数定义上一行,就是”把下面这个函数交给 timed 处理”。代码干净多了。
带参数的装饰器
有时候装饰器自己也要参数,比如”日志级别”。做法:再包一层:
def log_with(level):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{level}] 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log_with("INFO")
def hello():
print("你好")
hello() # [INFO] 调用 hello / 你好
结构:log_with("INFO") 先执行,返回 decorator;decorator 再接收函数。三层嵌套,理解”每一层返回下一层”即可。
functools.wraps:别弄丢函数信息
装饰后有个副作用:函数的名字、文档字符串变成了 wrapper 的:
def timed(func):
def wrapper(*args, **kwargs):
...
return wrapper
@timed
def slow_add(a, b):
"""两个数相加"""
...
print(slow_add.__name__) # wrapper(应该是 slow_add!)
print(slow_add.__doc__) # None(应该是 两个数相加!)
解决办法:装饰器内部用 functools.wraps:
from functools import wraps
def timed(func):
@wraps(func) # 把原函数的名字、文档等复制过来
def wrapper(*args, **kwargs):
...
return wrapper
写装饰器时永远记得加 @wraps,这是规范。
实战:三个常用装饰器
from functools import wraps
import time
# 1. 重试:失败自动重试 N 次
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e # 记住最后一次异常
print(f"第 {i+1} 次失败:{e}")
raise last_error # 重试耗尽,把最后一次异常抛出去
return wrapper
return decorator
@retry(times=3)
def unstable_api():
import random
if random.random() < 0.6:
raise ConnectionError("网络抖动")
return "成功"
print(unstable_api()) # 失败重试,最终成功
# 2. 缓存:相同参数不重复计算(记忆化)
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(30)) # 832040,秒出(不缓存的话要算半天)
# 3. 权限检查
def require_login(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not getattr(user, "logged_in", False):
raise PermissionError("请先登录")
return func(user, *args, **kwargs)
return wrapper
class User:
def __init__(self, logged_in):
self.logged_in = logged_in
@require_login
def view_profile(user):
return "个人资料页"
print(view_profile(User(True))) # 个人资料页
# view_profile(User(False)) # PermissionError: 请先登录
多个装饰器叠加
一个函数可以叠多个装饰器,从上往下装饰,从下往上执行:
@timed
@log_with("INFO")
def work():
time.sleep(0.1)
print("干活中")
work()
执行顺序:work = timed(log_with(“INFO”)(work))。调用时先经过 timed 的 wrapper,再进 log 的 wrapper,最后才到原函数。离函数最近的装饰器先执行。
新手坑
坑 1:忘加 @wraps。函数名、文档字符串全丢,调试时一脸懵。
坑 2:装饰器忘 return。装饰器函数必须返回一个函数,返回 None 的话原函数就变成 None 了。
坑 3:带参数装饰器少包一层。@log_with(“INFO”) 这种如果 decorator 只写两层,会报”参数类型错误”——数清楚层数。
小结
- 函数是一等公民,可赋值、可嵌套、可返回(闭包)
- 装饰器 = 接收函数、返回新函数的函数;
@装饰器是语法糖 - 带参数装饰器三层嵌套;多个装饰器从下往上执行
- 写装饰器永远加
@wraps(func) - 经典应用:计时、日志、重试、缓存、权限检查
练习
- 写一个
@logged装饰器:调用前后各打印一行日志(函数名 + 参数) - 写一个
@slow_down装饰器:调用前 sleep 1 秒(提示:wrapper 里 time.sleep(1)) - 思考:为什么装饰器返回的 wrapper 要用
*args, **kwargs接参数?如果原函数有默认参数或关键字参数,漏掉会怎样?