如何在Python中使用装饰器复制方法类型提示?

问题描述 投票:0回答:1

我想使用一个装饰器,它将方法签名复制到另一个类方法中,如下所示:

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
类型(使用 Pyright 检查,即安装了所有推荐的 Python 扩展的 VSCode Pylance)

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 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[SP, TR, TS]):
    def __call__(_, self: TS, *args: SP.args, **kwargs: SP.kwargs) -> TR: ...
class TargetMethod(Protocol[TR, TS]):
    def __call__(_, self: TS, *args: Any, **kwargs: Any) -> TR: ...

def copy_method_signature(source: SourceMethod[SP, 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

更新: Pyright Playground 上的改编代码用于 @blhsing 答案 - 基本上所有相同的问题,但实施时间更短 (无法添加评论 - 链接太长)

python python-typing pyright
1个回答
0
投票

您可以调整 this answer 中的装饰器工厂,以从另一个方法复制参数的类型提示,同时使用

Concatenate
保留
self
的类型和目标方法的返回类型:

from collections.abc import Callable
from typing import ParamSpec, TypeVar, Concatenate, Any

SP = ParamSpec('SP')
TR = TypeVar('TR')
TS = TypeVar('TS')

def copy_method_signature(
    f: Callable[Concatenate[Any, SP], Any]
) -> Callable[
    [Callable[Concatenate[TS, ...], TR]], Callable[Concatenate[TS, SP], TR]
]:
    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:
        ...

使用 Pyright 进行演示 此处

© www.soinside.com 2019 - 2024. All rights reserved.