【问题标题】:Remove Self from Callable Type signature to match Instance method从 Callable Type 签名中删除 Self 以匹配 Instance 方法
【发布时间】:2020-07-20 16:09:31
【问题描述】:

这样做的动机是检查事件处理程序的类型,以确保已注册事件期望作为参数的类型与处理程序准备提供的类型之间匹配。

我正在尝试在基于类的函数装饰器的类型注释中跟踪函数签名。这只是一个 mypy 存根项目:实际实现将以不同的方式获得相同的结果。

所以,我们有一个像这样的基本装饰器骨架

from typing import Any, Callable, Generic, TypeVar

FuncT = TypeVar("FuncT", bound=Callable)

class decorator(Generic[FuncT]):
    def __init__(self, method: FuncT) -> None:
        ... # Allows mypy to infer the parameter type

    __call__: FuncT

    execute: FuncT

使用以下存根示例

class Widget:
    def bar(self: Any, a: int) -> int:
        ...

    @decorator
    def foo(self: Any, a: int) -> int:
        ...

w = Widget()

reveal_type(Widget.bar)
reveal_type(w.bar)

reveal_type(Widget.foo.__call__)
reveal_type(w.foo.__call__)

揭示的类型如下:

Widget.bar (undecorated class method):         'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.bar (undecorated instance method):           'def (a: builtins.int) -> builtins.int'
Widget.foo.__call__ (decorated class method):  'def (self: demo.Widget, a: builtins.int) -> builtins.int'
w.foo.__call__ (decorated instance method):    'def (self: demo.Widget, a: builtins.int) -> builtins.int'

这意味着如果我调用w.bar(2) 它会通过类型检查器,但是如果我调用w.foo(2)w.foo.execute(2) 那么mypy 会抱怨没有足够的参数。同时Widget.bar(w, 2)Widget.foo(w, 2)Widget.foo.execute(w, 2)都通过了。

我所追求的是一种对此进行注释以说服w.foo.__call__w.foo.execute 给出与w.bar 相同的签名的方法。

【问题讨论】:

    标签: python-3.x types mypy


    【解决方案1】:

    我似乎不可能像我想要的那样干净利落。当前的类型系统只是简单地使用not have the capability 来混淆参数。

    我能找到的最接近的方法使用了两个技巧:一个大的重载来枚举(合理的)输入类型,以及定义__get__ 来区分类和实例访问。

    class DecoratorCallable(Generic[FuncT]):
        __call__ : FuncT
        execute  : FuncT
    
    # FuncT and FuncT2 refer to the method signature with and without self
    class DecoratorBase(Generic[FuncT, FuncT2]):
        @overload
        def __get__(self, instance: None, owner: object) -> DecoratorCallable[FuncT]:
            # when a method is accessed directly, instance will be None
            ...
        @overload
        def __get__(self, instance: object, owner: object) -> DecoratorCallable[FuncT2]:
            # when a method is accessed through an instance, instance will be that object
            ...
    
        def __get__(self, instance: Optional[object], owner: object) -> DecoratorCallable:
            ...
    
    # To support methods with up to 5 parameters, define 5 type vars. 
    # For more parameters, just keep counting.
    T1 = TypeVar("T1")
    T2 = TypeVar("T2")
    T3 = TypeVar("T3")
    T4 = TypeVar("T4")
    T5 = TypeVar("T5")
    R = TypeVar("R")
    
    @overload
    def decorator(f: Callable[[T1], R]) -> DecoratorBase[Callable[[T1], R], Callable[[], R]] :...
    @overload
    def decorator(f: Callable[[T1, T2], R]) -> DecoratorBase[Callable[[T1, T2], R], Callable[[T2], R]] :...
    @overload
    def decorator(f: Callable[[T1, T2, T3], R]) -> DecoratorBase[Callable[[T1, T2, T3], R], Callable[[T2, T3], R]] :...
    @overload
    def decorator(f: Callable[[T1, T2, T3, T4], R]) -> DecoratorBase[Callable[[T1, T2, T3, T4], R], Callable[[T2, T3, T4], R]] :...
    @overload
    def decorator(f: Callable[[T1, T2, T3, T4, T5], R]) -> DecoratorBase[Callable[[T1, T2, T3, T4, T5], R], Callable[[T2, T3, T4, T5], R]] :...
    
    def decorator(f: Callable[..., R]) -> DecoratorBase[Callable, Callable]:
        ...
    

    与以前相同的类,现在显示的类型是

    Widget.bar (undecorated class method):         'def (self: Any, a: builtins.float) -> builtins.bool'
    w.bar (undecorated instance method):           'def (a: builtins.float) -> builtins.bool'
    Widget.foo.__call__ (decorated class method):  'def (Any, builtins.float*) -> builtins.bool*'
    w.foo.__call__ (decorated instance method):    'def (builtins.float*) -> builtins.bool*'
    

    这至少意味着 Mypy 将正确地允许 Widget.foo(w, 2)w.foo(2),并正确地禁止 Widget.foo(w, "A")w.foo("A")

    但是,在此过程中,它丢失了参数名称,因此w.foo(a=2) 得到了一个无用的错误“意外的关键字参数“a””。

    如果你试图用太多参数装饰一个方法,它也会失败,并且可能在一些奇怪的边缘情况下。

    【讨论】:

    • 实际上在同一个github链接上提到,ParamSpec和朋友们的开发已经取得了相当大的进展。虽然还有很多事情要解决,但 T1 的质量……应该可以在不久的将来更换。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-15
    • 2021-06-12
    • 1970-01-01
    • 2016-07-02
    • 2019-11-07
    相关资源
    最近更新 更多