现代Python学习——深入理解 Python 装饰器
现代Python学习——深入理解 Python 装饰器
装饰器是 Python 的强大特性:它能在不改动被装饰函数源代码的情况下为函数「包装」通用行为(日志、计时、缓存、重试、权限校验等)。在开启 Pylance strict 的编辑器环境下,我们还要注意类型注解写法——用 ParamSpec + TypeVar 保留被装饰函数的准确签名,避免 Callable 被当成未指定类型参数的泛型而飘红。当然,下面是这些概念的简单陈述:
- 装饰器(decorator):接受一个可调用对象(通常是函数),返回一个新的可调用对象。语法糖:
@decorator写在函数定义上。 - 装饰器工厂(decorator factory):带参数的装饰器,其本身是一个返回装饰器的函数,语法
@factory(arg)。 - 类装饰器:用类实现的装饰器,通常通过实现
__call__保存状态。 - 保持元信息:用
functools.wraps(或update_wrapper)保留原函数__name__、__doc__、__annotations__等。 - 静态类型(Pylance strict):使用
ParamSpec+TypeVar精确保留被装饰函数的参数和返回类型,避免裸Callable导致的reportMissingTypeArgument。
在 strict 模式中,尽量不要使用裸 Callable,应使用 ParamSpec + TypeVar 保留签名:
# typing_compat.py — 兼容写法(可直接 copy)
try:
from typing import ParamSpec, TypeVar, Callable, Awaitable
from typing import Concatenate # 可选:用于显式在签名中插入 self/class
except Exception:
# Python < 3.10
from typing_extensions import ParamSpec, TypeVar, Concatenate # type: ignore
from typing import Callable, Awaitable # Callable/Awaitable 存在于 typing
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
用法约定:
- 通用同步装饰器类型:
Callable[P, R] -> Callable[P, R] - 通用异步装饰器类型:
Callable[P, Awaitable[T]] -> Callable[P, Awaitable[T]] - 装饰器工厂返回类型示例:
Callable[[Callable[P, R]], Callable[P, R]]
语法与示例
1) 最简单的计时器(严格类型)
import functools
import time
from typing import Callable
try:
from typing import ParamSpec, TypeVar
except Exception:
from typing_extensions import ParamSpec, TypeVar # type: ignore
P = ParamSpec("P")
R = TypeVar("R")
def time_calculator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"[time_calculator] {func.__name__} took {end_time - start_time:.6f}s")
return result
return wrapper
2) 带参数的装饰器(装饰器工厂)
def repeat(n: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last: R
for _ in range(n):
last = func(*args, **kwargs)
return last
return wrapper
return decorator
3) 异步函数的装饰器(保留 awaitable 签名)
import asyncio
from typing import Awaitable
def async_time_calculator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
loop = asyncio.get_event_loop()
start = loop.time()
result = await func(*args, **kwargs)
end = loop.time()
print(f"[async_time_calculator] {func.__name__} took {end - start:.6f}s")
return result
return wrapper
4) 类装饰器(有状态)
class CountCalls:
def __init__(self, func: Callable[P, R]) -> None:
functools.update_wrapper(self, func)
self._func = func
self.count = 0
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
self.count += 1
return self._func(*args, **kwargs)
5) 在方法上使用装饰器 & 显式 self 类型(使用 Concatenate)
当你想显式在类型签名中表示 self 时(strict 下更严格),可以用 Concatenate:
try:
from typing import Concatenate
except Exception:
from typing_extensions import Concatenate # type: ignore
S = TypeVar("S") # self 的类型
def method_logger(func: Callable[Concatenate[S, P], R]) -> Callable[Concatenate[S, P], R]:
@functools.wraps(func)
def wrapper(self: S, *args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__} on {self!r}")
return func(self, *args, **kwargs)
return wrapper
class Greeter:
@method_logger
def greet(self, name: str) -> str:
return f"hi {name}"
Concatenate对于把self或cls明确插入签名会很有帮助,但它是 Python 3.10+ 的特性;在旧版本用typing_extensions回退。
装饰器行为细节(实践中常见的“坑”)
-
装饰器顺序:多个装饰器从上到下应用时,实际调用顺序是从下到上。例如:
@deco_a @deco_b def f(...): ...等价于
f = deco_a(deco_b(f))。 -
保持签名:
functools.wraps除了拷贝元信息,还拷贝__annotations__(有助于类型检查器)。在 strict 模式下务必使用wraps。 -
同步装饰器不能直接包裹 async 函数(否则会返回 coroutine 对象但不会
await)。对 async 函数写专门的 async 装饰器或让装饰器返回 coroutine。 -
类型检查器与运行时不一致:类型注解仅供静态检查。装饰器在运行时仍要保证行为兼容原函数签名(不要随意改变参数传递方式)。
-
方法装饰器与 descriptor:如果需要在装饰器中实现 descriptor 行为(例如把函数变成 property),要注意
__get__的实现;对于常见用例,可优先考虑@property或@classmethod/@staticmethod的组合。
为什么要做类型注解(Pylance strict)
Pylance strict 会对泛型和可调用注解很严格。常见飘红原因之一就是使用裸 Callable(即 Callable 没有类型参数),Pylance 会报 reportMissingTypeArgument。
为了解决并且不丢失被装饰函数的签名信息,推荐使用:
ParamSpec:表示函数参数列表(比如(*args, **kwargs)的类型信息)TypeVar:表示返回值类型
组合能写出 Callable[P, R] 等精确注解。
支持Pylance Strict的规范的通用模板(兼容 typing_extensions)
下面给出一个开箱即用的模板,兼容 Python 3.10+(原生 ParamSpec)和更早 Python(从 typing_extensions 导入):
# utils/decorator_typing.py
import functools
import time
from typing import Callable, TypeVar
# 兼容:若 Python 版本较低,可从 typing_extensions 导入 ParamSpec
try:
from typing import ParamSpec # Python 3.10+
except Exception:
from typing_extensions import ParamSpec # type: ignore
P = ParamSpec("P")
R = TypeVar("R")
举个例子:一个通用、严格类型的计时器装饰器
# timing_decorator.py
import functools
import time
from typing import Callable, TypeVar
try:
from typing import ParamSpec # Python 3.10+
except Exception:
from typing_extensions import ParamSpec # type: ignore
P = ParamSpec("P")
R = TypeVar("R")
def time_calculator(func: Callable[P, R]) -> Callable[P, R]:
"""
通用计时器装饰器。使用 ParamSpec + TypeVar 保留原函数签名与返回类型。
"""
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time: float = time.time()
result: R = func(*args, **kwargs)
end_time: float = time.time()
elapsed = end_time - start_time
print(f"[time_calculator] {func.__name__} took {elapsed:.6f} seconds")
return result
return wrapper
# 用法示例
@time_calculator
def add(a: int, b: int) -> int:
time.sleep(0.2)
return a + b
@time_calculator
def say(msg: str) -> None:
time.sleep(0.1)
print(msg)
说明:
Callable[P, R]明确了传入函数的参数和返回值类型,Pylance strict 不会再飘红,IDE 也能正确推断被装饰函数的签名(例如add仍然被认为接受int, int并返回int)。
带参数的装饰器(装饰器工厂)
带参数的装饰器需要额外一层包装。下面示例是 repeat(n):执行函数 n 次并返回最后一次结果。
from typing import Callable, TypeVar
try:
from typing import ParamSpec
except Exception:
from typing_extensions import ParamSpec # type: ignore
import functools
import time
P = ParamSpec("P")
R = TypeVar("R")
def repeat(n: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last: R
for _ in range(n):
last = func(*args, **kwargs)
return last
return wrapper
return decorator
# 用法:
@repeat(3)
def greet(name: str) -> str:
print("greet", name)
return f"hello {name}"
类型注解 Callable[[Callable[P, R]], Callable[P, R]] 精确标注了工厂返回类型,Pylance strict 不会警告。
装饰异步函数(async)——严格类型
如果被装饰函数是 async def(返回 Awaitable[T]),我们也可以写出严格的注解。
import asyncio
import functools
from typing import Callable, TypeVar, Awaitable
try:
from typing import ParamSpec
except Exception:
from typing_extensions import ParamSpec # type: ignore
P = ParamSpec("P")
T = TypeVar("T")
def async_time_calculator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
start_time = asyncio.get_event_loop().time()
result = await func(*args, **kwargs)
end_time = asyncio.get_event_loop().time()
print(f"[async_time_calculator] {func.__name__} took {end_time - start_time:.6f} seconds")
return result
return wrapper
# 用法:
@async_time_calculator
async def async_add(a: int, b: int) -> int:
await asyncio.sleep(0.1)
return a + b
注意:此处 Callable[P, Awaitable[T]] -> Callable[P, Awaitable[T]] 明确声明了这是装饰异步函数的签名。
类装饰器(用类实现的装饰器)
类装饰器有时用于需要保存状态的场景。类型注解写法也可以很严格:
from typing import Callable, TypeVar
try:
from typing import ParamSpec
except Exception:
from typing_extensions import ParamSpec # type: ignore
import functools
P = ParamSpec("P")
R = TypeVar("R")
class CountCalls:
def __init__(self, func: Callable[P, R]) -> None:
functools.update_wrapper(self, func)
self._func = func
self.count = 0
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
self.count += 1
return self._func(*args, **kwargs)
# 用法
@CountCalls
def foo(x: int) -> int:
return x * 2
Pylance 会知道 CountCalls 被用于函数并不会飘红(注意 __call__ 用上 P.args / P.kwargs)。
在方法上使用装饰器
方法(实例方法)本质上多了一个 self 参数,使用 ParamSpec 时签名会把 self 包含进去,因此无需特殊处理:
class Greeter:
@time_calculator
def greet(self, name: str) -> None:
print("Hello", name)
Pylance strict 能正常推断 greet(self, name: str) -> None。
组合装饰器与 functools.wraps
始终在 wrapper 上使用 functools.wraps 来保留 __name__、__doc__、__annotations__ 等。对于类型检查器和调试体验都大有裨益。
5) 注意事项与兼容性
- ParamSpec 支持:
- Python 3.10+:
from typing import ParamSpec - Python 3.8/3.9:
from typing_extensions import ParamSpec - 上面示例已包含兼容写法(
try / except)。
- Python 3.10+:
- 不要使用裸
Callable(会触发reportMissingTypeArgument)。始终写成Callable[..., Any](若你确实要接受任意签名),但更好是用ParamSpec保留签名:Callable[P, R]。 - async 装饰器:同步装饰器不能直接包裹 async 函数(因为 wrapper 不是 async 会返回 coroutine 而非 awaited 结果)。为 async 函数写专用的 async 装饰器,类型签名应使用
Awaitable[T]。 - 第三方类型工具:若你使用
mypy,ParamSpec / TypeVar 的写法也能被正确识别。若你的 CI 运行在旧环境,请确保typing_extensions在依赖里。 - 保留返回值类型:装饰器应该尽量返回与原函数相同类型的结果(除非故意改变),这样静态类型检查器才好推断。
更多推荐


所有评论(0)