Python:functools.wraps能否用于非装饰器函数?lru_cache类型提示修复
我有如下函数:
def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: ... # 此处执行逻辑并返回值
在VSCode中调用该函数时,能看到完整的类型提示:
(function) def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool
由于该函数调用耗时,我想用functools.lru_cache装饰它(Python3.8环境):
@functools.lru_cache() def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: ...
但装饰后,原有类型提示被覆盖,变成了:
(function) load_resources: _lru_cache_wrapper[Unknown]
于是我尝试用functools.wraps包装:
def __load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: ... @functools.wraps(__load_resources) @functools.lru_cache() def load_resources(*args, **kwargs): return __load_resources(*args, **kwargs)
却得到了奇怪的类型提示:
(function) load_resources: _Wrapped[(__load_resources(path: Path, src_type: str, *, load_all: bool = False), bool, (*args: Hashable, **kwargs: Hashable), Unknown]
我知道__name__元数据有问题,但原以为其他特性会正常生效。想问两个问题:
- 非装饰器场景下使用
functools.wraps是否错误? - 如何为被
functools.lru_cache装饰的函数添加正确的类型提示?
我的临时实现方式如下:
def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: return __get_context(path, src_type, load_all=load_all)
1. 非装饰器场景用functools.wraps是否错误?
不是错误,但你的用法有问题。functools.wraps的核心作用是复制原函数的元数据(比如__name__、__doc__、__annotations__等),但它不负责类型签名的推断。你之前把load_resources定义成*args, **kwargs的签名,和原函数__load_resources的精确签名不匹配,类型检查器无法从可变参数关联到原函数的参数类型,所以才出现混乱的提示。
如果一定要用wraps,需要保证包装函数的签名和原函数完全一致,而不是用可变参数:
from functools import lru_cache, wraps from pathlib import Path def __load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: ... @wraps(__load_resources) @lru_cache() def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: return __load_resources(path, src_type, load_all=load_all)
这样既保留了缓存功能,wraps也能正确复制元数据,类型检查器也能识别到正确的签名。
2. 为lru_cache装饰的函数添加正确类型提示的方法
针对Python3.8,推荐两种简洁可靠的方式:
方式一:对外暴露精确签名的函数,内部调用带缓存的私有函数
这和你的临时实现思路一致,只是把私有函数加上lru_cache装饰,对外的函数保持原有的精确签名:
from functools import lru_cache from pathlib import Path @lru_cache() def __load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: # 原函数逻辑 ... def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: return __load_resources(path, src_type, load_all=load_all)
这种方式最直观,类型检查器能直接识别load_resources的完整签名,同时私有函数的缓存功能正常生效,完全避免了装饰器导致的类型提示丢失问题。
方式二:用typing.cast结合Protocol强制指定类型(适合不想拆分函数的场景)
如果不想拆分函数,可以用typing.Protocol定义和原函数一致的签名,再通过cast告诉类型检查器装饰后的函数符合该签名:
from functools import lru_cache from pathlib import Path from typing import cast, Protocol # 定义匹配原函数签名的Protocol class LoadResourcesProtocol(Protocol): def __call__(self, path: Path, src_type: str, *, load_all: bool = False) -> bool: ... @lru_cache() def load_resources(path: Path, src_type: str, *, load_all: bool = False) -> bool: # 原函数逻辑 ... # 强制转换类型,让类型检查器识别正确的签名 load_resources = cast(LoadResourcesProtocol, load_resources)
这种方式不需要拆分函数,通过显式的类型声明让类型检查器忽略lru_cache包装器的干扰,识别到原函数的签名。
内容的提问来源于stack exchange,提问作者jolim

