Python装饰器实战:从入门到封装通用业务逻辑

张开发
2026/4/16 23:01:56 15 分钟阅读

分享文章

Python装饰器实战:从入门到封装通用业务逻辑
一个让你头皮发麻的场景想象一下这个画面你写了一个爬虫每天要抓几千条数据。刚开始跑得好好的结果第三天网站把你的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我猜你注意到上面代码里的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_attempts3, delay1): 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_attempts3, delay2) def unstable_request(): # 可能会失败的请求 pass这里的关键是retry(max_attempts3)这个语法糖实际上是先调用retry(3)返回一个装饰器再用这个装饰器去装饰函数。三层函数各司其职。实战升华封装通用业务逻辑回到开头那个爬虫场景。你现在有了计时、重试还差个代理IP轮换。而且你可能想把这三个功能组合起来让每个爬虫函数都自动带上这些能力。这就是装饰器的真正威力——把通用逻辑从业务代码中剥离出来。先从代理IP说起。假设你用了站大爷的隧道代理服务它能自动帮你轮换IP避免被封。按照官方文档使用时需要配置代理参数proxies { http: http://username:passwordproxy.zdaye.com:port, https: http://username:passwordproxy.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_attempts3, delay1) use_proxy def fetch_page(url, **kwargs): proxies kwargs.get(proxies) response requests.get(url, proxiesproxies, timeout10) 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:passproxy.zdaye.com:8888) def fetch_page(url, **kwargs): # ...这样代理配置和业务逻辑彻底分离换个代理只需要改这一行配置。总结装饰器到底解决了什么问题回到开头的场景。如果没有装饰器你需要在每个爬虫函数里写代理获取、异常重试、计时日志代码又臭又长改一个需求要改十几个文件。有了装饰器你把通用逻辑封装成一个个“帽子”retry、timer、use_proxy扣在函数头上就完事。业务函数只关心核心逻辑其他脏活累活全部交给装饰器。这背后是软件工程的一个核心原则关注点分离。把“做什么”和“怎么做”分开代码才能清爽、好维护。装饰器就是实现这种分离的利器。下次你再遇到重复的代码逻辑不妨停下来想一想这东西能不能写成装饰器如果答案是“能”那你就找到了让代码脱胎换骨的钥匙。

更多文章