Python装饰器与闭包

时游大约 3 分钟

装饰器与闭包

函数是一等公民

Python 中函数和普通对象一样:可以赋值给变量、作为参数传递、作为返回值返回。这是理解装饰器的前提。

# 函数可以赋值给变量
def shout(text):
    return text.upper()


speak = shout  # 注意没有括号,传递的是函数本身
print(speak("hello"))  # HELLO

# 函数作为参数
def apply(func, value):
    return func(value)


print(apply(shout, "hi"))  # HI

# 函数作为返回值
def make_multiplier(n):
    def multiply(x):
        return x * n
    return multiply  # 返回内层函数


double = make_multiplier(2)
print(double(5))  # 10

闭包

闭包 = 内层函数 + 引用的外层函数变量。内层函数「记住」了定义时的环境变量,即使外层函数已经执行结束。

def make_counter():
    count = 0  # 外层函数的局部变量

    def counter():
        nonlocal count  # 声明使用外层的count,没有这行则只能读不能改
        count += 1
        return count

    return counter


counter = make_counter()  # make_counter已执行结束,但count仍被counter记住
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

# 每次调用make_counter都创建独立的闭包环境
counter2 = make_counter()
print(counter2())  # 1,和counter互不干扰

装饰器基础

装饰器接收一个函数,返回一个增强版的新函数。@语法糖等价于 func = decorator(func),本质是闭包的应用。

import time

# 手写一个计时装饰器
def timer(func):
    def wrapper(*args, **kwargs):  # *args/**kwargs保证兼容任意参数
        start = time.time()
        result = func(*args, **kwargs)  # 调用原函数
        end = time.time()
        print(f"{func.__name__} 耗时 {end - start:.4f} 秒")
        return result
    return wrapper


@timer  # 等价于 slow_task = timer(slow_task)
def slow_task():
    time.sleep(0.5)
    return "done"


print(slow_task())  # slow_task 耗时 0.5xxx 秒 -> done

functools.wraps:保留函数元信息

被装饰后,函数的名字和文档字符串会被 wrapper 覆盖。@functools.wraps 把原函数的元信息复制到 wrapper 上,是写装饰器的标准姿势。

import functools

def logger(func):
    @functools.wraps(func)  # 没有这行,下面的__name__会是'wrapper'
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__},参数:{args} {kwargs}")
        return func(*args, **kwargs)
    return wrapper


@logger
def add(a, b):
    """两数相加"""
    return a + b


add(1, b=2)  # 调用 add,参数:(1) {'b': 2}
print(add.__name__)  # add(没有wraps则是wrapper)
print(add.__doc__)   # 两数相加

带参数的装饰器

装饰器本身需要参数时,在外面再包一层函数:三层结构,最外层接收参数、返回装饰器、装饰器返回 wrapper。

import functools

def repeat(times):  # 第一层:接收装饰器参数
    def decorator(func):  # 第二层:接收被装饰函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs):  # 第三层:实际执行
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator


@repeat(times=3)  # 带参数的装饰器,函数会重复执行3次
def greet(name):
    print(f"hello {name}")


greet("时游")  # 打印3次 hello 时游

装饰器的常见应用

1. 重试机制(爬虫场景)

import functools
import time
import requests

def retry(times=3, delay=1):
    """请求失败自动重试"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except requests.exceptions.RequestException as err:
                    print(f"第{attempt}次请求失败:{err}")
                    if attempt == times:
                        raise  # 重试耗尽,抛出异常
                    time.sleep(delay)
        return wrapper
    return decorator


@retry(times=3, delay=2)
def fetch(url):
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    return response.text

2. 缓存计算结果

import functools

# lru_cache:缓存函数结果,相同参数只计算一次
@functools.lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)


print(fib(50))  # 12586269025,瞬间完成;不加缓存则是天文数字级的重复计算

类中的常用装饰器

装饰器作用
@staticmethod静态方法,不需要实例,挂在类命名空间下的普通函数
@classmethod类方法,第一个参数是类本身(cls),常用于替代构造函数
@property把方法伪装成属性访问,可在赋值时校验(见面向对象)
@functools.lru_cache缓存函数返回值,相同参数直接返回缓存
@functools.wraps装饰器专用,保留原函数的名字和文档字符串

装饰器可以叠加使用,执行顺序为「就近原则」:@a @b def f 相当于 a(b(f)),b 先包装 f,a 再包装外层。

上次编辑于:
贡献者: 15327360835
Loading...