我想使用一个装饰器,它将方法签名复制到另一个类方法中,如下所示:
class A:
def func_source(self, x: str, y: int = 0) -> None: ...
class B:
@copy_method_signature(A.func_source)
def func_target(self, *args, **kwargs) -> int: ...
期望
B.func_target
显示与 A.func_source
相同的类型提示,除了 self
和 return
类型(使用 VSCode Pylance 检查并安装所有推荐的 Python 扩展)
B.func_target(...) # (self: B, x: str, y: int = 0) -> int
b = B()
b.func_target(...) # (x: str, y: int = 0) -> int
typing.Concatenate
主要从 python/typing GitHub Issues 借用这个装饰器:'提案:kwargs 的签名复制':
from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import Any, Concatenate, ParamSpec, TypeVar
SP = ParamSpec("SP") # Source Parameters
TR = TypeVar("TR") # Target Return
TS = TypeVar("TS") # Target Self
def copy_method_signature(source: Callable[Concatenate[Any, SP], Any]):
def wrapper(target: Callable[Concatenate[TS, ...], TR]) -> Callable[Concatenate[TS, SP], TR]:
@wraps(source)
def wrapped(self: TS, *args: SP.args, **kwargs: SP.kwargs) -> TR:
return target(self, *args, **kwargs)
return wrapped
return wrapper
我改变的两件事是:
func_target
不会复制 func_source
返回类型 - 它保留 int
而不是 func_source
的 None
(在 TR 中传递)func_target
正确显示其 self
参数类型 - 它保留 B
而不是原始实现中的 Any
(在 TS 中传递)问题是:
B.func_target()
预计会提示(self: B, x: str, y: int = 0) -> int
,
但提示 (B, x: str, y: int = 0) -> int
第一个参数缺少其名称b.func_target()
预计会突出显示 x: str
作为第一个参数,但突出显示 y: int = 0
(x: str, y: int = 0) -> int
typing.Protocol
这些问题的一部分在上面的同一个线程中提到,并提出了下一个答复的可能解决方案 - 使用 Callback
typing.Protocol
而不是 typing.Callable[...]
语法与 typing.Concatenate
定义可调用类型应该保留 B.target_func
' s self
参数名称和类型。
这就是我想出的解决方案:
... # all imports from above
from typing import Generic, Protocol
# pylint: disable = no-self-argument, typevar-name-incorrect-variance
SP = ParamSpec("SP") # Source Parameters
TR = TypeVar("TR", covariant=True) # Target Return
TS = TypeVar("TS", contravariant=True) # Target Self
class SourceMethod(Protocol, Generic[SP, TR, TS]):
def __call__(_, self: TS, *args: SP.args, **kwargs: SP.kwargs) -> TR: ...
class TargetMethod(Protocol, Generic[TR, TS]):
def __call__(_, self: TS, *args: Any, **kwargs: Any) -> TR: ...
def copy_method_signature(source: SourceMethod[SP, Any, Any], Any]):
def wrapper(target: TargetMethod[TR, TS]) -> SourceMethod[SP, TR, TS]:
@wraps(source)
def wrapped(self: TS, *args: SP.args, **kwargs: SP.kwargs) -> TR:
return target(self, *args, **kwargs)
return wrapped
return wrapper
从技术上来说,没有必要更换每个
Callable[Concatenate[...], ...]
,唯一看起来至关重要的地方是
...
def wrapper(target: TargetMethod[TR, TS]) -> SourceMethod[SP, TR, TS]:
^^^^^^^^^^^^^^^^^^^^^^^^
... this one
结果如下:
B.func_target()
按预期工作 - (self: B, x: str, y: int = 0) -> int
b.func_target()
预计会提示(x: str, y: int = 0) -> int
,但是提示(self: B, x: str, y: int = 0) -> int
解决这些问题的进一步尝试没有取得任何进展,我真的陷入了困境
附注 顺便说一句,来自
第一个链接的
@copy_callable_signature
装饰器在从另一个普通函数复制签名的普通函数和从同一类的方法复制的方法上都运行得很好。以防万一:
from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import Any, Concatenate, ParamSpec, TypeVar
SP = ParamSpec("SP") # Source Parameters
TR = TypeVar("TR") # Target Return
def copy_callable_signature(source: Callable[SP, Any]):
def wrapper(target: Callable[..., TR]) -> Callable[SP, TR]:
@wraps(source)
def wrapped(*args: SP.args, **kwargs: SP.kwargs) -> TR:
return target(*args, **kwargs)
return wrapped
return wrapper
您可以使用这样的装饰器工厂从另一个方法复制参数的类型提示:
from collections.abc import Callable
from typing import ParamSpec, TypeVar, Concatenate, Any
P = ParamSpec('P')
R = TypeVar('R')
def copy_method_signature(
f: Callable[Concatenate[Any, P], Any]
) -> Callable[[Callable[..., R]], Callable[Concatenate[Any, P], R]]:
return lambda _: _ # type: ignore[return-value]
class A:
def func_source(self, x: str, y: int = 0) -> None:
...
class B:
@copy_method_signature(A.func_source)
def func_target(self, *args, **kwargs) -> int:
...
B().func_target('test')
使用 Pyright 进行演示 此处