代号转生成为魔塔
52.35M · 2026-04-02
想象一下这个画面:你写了一个爬虫,每天要抓几千条数据。刚开始跑得好好的,结果第三天,网站把你的IP封了。于是你加了个代理池,每次请求换一个IP。代码倒是能跑,但看起来让人想哭:
def fetch_data(url):
# 获取代理
proxy = get_proxy()
try:
response = requests.get(url, proxies={"http": proxy, "https": proxy})
# ... 业务逻辑
except Exception as e:
# 换一个代理重试
proxy2 = get_proxy()
response = requests.get(url, proxies={"http": proxy2, "https": proxy2})
更可怕的是,你有十几个类似的函数,每个都要写一遍代理获取、异常重试的逻辑。代码重复得像复读机,改一个地方得改十几个地方。这时候你开始怀念——要是能有个东西,把这些“脏活累活”抽出来,干净利落地加到每个函数上,该多好?
恭喜你,你需要的就是装饰器。
说白了,装饰器就是一个“函数包装机”。给你一个函数,它帮你包一层,加点额外功能,再还给你。原函数该干嘛还干嘛,但多了点“超能力”。
举个生活例子:你买了个手机,套了个手机壳。手机还是那个手机,打电话发微信的功能一点没变,但多了防摔、防滑的功能。装饰器就是这个手机壳。
Python里的装饰器长这样:
def my_decorator(func):
def wrapper():
print("手机壳:准备开始")
func()
print("手机壳:执行完毕")
return wrapper
@my_decorator
def call():
print("打电话中...")
call()
运行结果:
手机壳:准备开始
打电话中...
手机壳:执行完毕
看到没?@my_decorator 这行代码,就是给你的 call 函数“套了个壳”。等价于 call = my_decorator(call)。
回到开头那个爬虫场景,假设你想知道哪个函数跑得慢,方便优化。写个计时装饰器是最直接的:
import time
from functools import wraps
def timer(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
然后,任何你想监控的函数,头顶上加个 @timer 就行:
@timer
def crawl_page(url):
time.sleep(0.5) # 模拟网络请求
return f"抓取{url}成功"
crawl_page("example.com")
# 输出:crawl_page 耗时:0.50秒
这里有个细节:*args, **kwargs 是干嘛的?因为不同的函数参数个数不一样,这俩家伙能“接住”任意参数,然后原封不动传给原函数。这样你的装饰器就能用在任何函数上了,通用性拉满。
我猜你注意到上面代码里的 @wraps(func) 了。这玩意儿不是摆设,它能解决一个“身份危机”。
做个实验你就懂了:
def bad_decorator(func):
def wrapper():
func()
return wrapper
@bad_decorator
def hello():
"""我是文档"""
pass
print(hello.__name__) # 输出:wrapper
print(hello.__doc__) # 输出:None
被装饰后,函数的名字和文档全丢了!调试的时候你看到 wrapper 这个函数名,根本不知道它是谁。这就好比一个人戴了面具,你认不出他是谁。
加上 @wraps 之后:
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper():
func()
return wrapper
@good_decorator
def hello():
"""我是文档"""
pass
print(hello.__name__) # 输出:hello
print(hello.__doc__) # 输出:我是文档
信息全回来了。所以,写装饰器一定带上 @wraps,这是专业和业余的分水岭。
有时候你想让装饰器“可配置”。比如重试次数,有的函数网络不稳定,想重试3次;有的函数只是偶尔失败,重试1次就够了。
这时候需要三层嵌套:
from functools import wraps
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"第{attempt}次失败,{delay}秒后重试...")
time.sleep(delay)
return None
return wrapper
return decorator
用起来很直观:
@retry(max_attempts=3, delay=2)
def unstable_request():
# 可能会失败的请求
pass
这里的关键是:@retry(max_attempts=3) 这个语法糖,实际上是先调用 retry(3) 返回一个装饰器,再用这个装饰器去装饰函数。三层函数,各司其职。
回到开头那个爬虫场景。你现在有了计时、重试,还差个代理IP轮换。而且,你可能想把这三个功能组合起来,让每个爬虫函数都自动带上这些能力。
这就是装饰器的真正威力——把通用逻辑从业务代码中剥离出来。
先从代理IP说起。假设你用了站大爷的隧道代理服务,它能自动帮你轮换IP,避免被封。按照官方文档,使用时需要配置代理参数:
proxies = {
"http": "http://username:password@proxy.zdaye.com:port",
"https": "http://username:password@proxy.zdaye.com:port"
}
你可以写一个装饰器,自动给每个请求带上代理:
def use_proxy(func):
@wraps(func)
def wrapper(*args, **kwargs):
proxies = {
"http": "http://你的用户名:你的密码@隧道地址:端口",
"https": "http://你的用户名:你的密码@隧道地址:端口"
}
# 把代理参数注入到kwargs里
kwargs['proxies'] = proxies
return func(*args, **kwargs)
return wrapper
然后,再加上重试和计时,三个装饰器叠在一起用:
@timer
@retry(max_attempts=3, delay=1)
@use_proxy
def fetch_page(url, **kwargs):
proxies = kwargs.get('proxies')
response = requests.get(url, proxies=proxies, timeout=10)
return response.text
看到没?fetch_page 这个函数本身只关注一件事:用给定的URL和代理去请求页面。至于“怎么计时、怎么重试、代理从哪来”,全交给装饰器了。
装饰器的执行顺序是从下往上:先 @use_proxy 加上代理,再 @retry 加上重试,最后 @timer 算总耗时。这样每个装饰器只干一件事,但组合起来威力巨大。
你有没有想过,代理信息(用户名、密码、地址)硬编码在装饰器里不太好?换个代理就得改代码,太low了。
把 use_proxy 也做成可配置的:
def proxy_config(proxy_url):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
kwargs['proxies'] = {
"http": proxy_url,
"https": proxy_url
}
return func(*args, **kwargs)
return wrapper
return decorator
# 使用
@proxy_config("http://user:pass@proxy.zdaye.com:8888")
def fetch_page(url, **kwargs):
# ...
这样,代理配置和业务逻辑彻底分离,换个代理只需要改这一行配置。
回到开头的场景。如果没有装饰器,你需要在每个爬虫函数里写代理获取、异常重试、计时日志,代码又臭又长,改一个需求要改十几个文件。
有了装饰器,你把通用逻辑封装成一个个“帽子”@retry、@timer、@use_proxy,扣在函数头上就完事。业务函数只关心核心逻辑,其他脏活累活全部交给装饰器。
这背后是软件工程的一个核心原则:关注点分离。把“做什么”和“怎么做”分开,代码才能清爽、好维护。装饰器就是实现这种分离的利器。
下次你再遇到重复的代码逻辑,不妨停下来想一想:这东西能不能写成装饰器?如果答案是“能”,那你就找到了让代码脱胎换骨的钥匙。