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 再包装外层。
Loading...
