一个让你头皮发麻的场景

想象一下这个画面:你写了一个爬虫,每天要抓几千条数据。刚开始跑得好好的,结果第三天,网站把你的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_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,扣在函数头上就完事。业务函数只关心核心逻辑,其他脏活累活全部交给装饰器。

这背后是软件工程的一个核心原则:关注点分离。把“做什么”和“怎么做”分开,代码才能清爽、好维护。装饰器就是实现这种分离的利器。

下次你再遇到重复的代码逻辑,不妨停下来想一想:这东西能不能写成装饰器?如果答案是“能”,那你就找到了让代码脱胎换骨的钥匙。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com