【问题标题】:How to combine a custom protocol with the Callable protocol?如何将自定义协议与 Callable 协议结合起来?
【发布时间】:2020-10-20 19:42:42
【问题描述】:

我有一个装饰器,它接受一个函数并返回带有一些附加属性的相同函数:

import functools
from typing import *


def decorator(func: Callable) -> Callable:
    func.attr1 = "spam"
    func.attr2 = "eggs"
    return func

如何键入提示decorator 的返回值?我希望类型提示传达两条信息:

  1. 返回值为Callable
  2. 返回值有属性attr1和attr2

如果我写一个协议,

class CallableWithAttrs(Protocol):
    attr1: str
    attr2: str

然后我输了Callable。显然我不能让协议继承自Callable;

class CallableWithAttrs(Callable, Protocol):
    attr1: str
    attr2: str

mypy 说:

error: Invalid base class "Callable"

另一方面,如果我只使用Callable,我会丢失有关添加属性的信息。



这在引入类型变量时可能更加复杂,即装饰器必须返回与给定函数 func 相同类型的可调用对象,正如 MisterMiyagi 在 cmets 中指出的那样。

import functools
from typing import *

C = TypeVar('C', bound=Callable)


def decorator(func: C) -> C:
    func.attr1 = "spam"
    func.attr2 = "eggs"
    return func

现在我该怎么办?我不能从类型变量继承:

class CallableWithAttrs(C, Protocol):
    attr1: str
    attr2: str
error: Invalid base class "C"

【问题讨论】:

  • Union[CallableWithAttrs, Callable]…?
  • @deceze 更像Intersection[CallableWithAttrs, Callable] (which doesn't exist).
  • 是的,确实如此。
  • 从您的decorator 的角度来看,func 是Callable 为什么重要?它从来没有这样称呼它,在鸭子打字之后,我看不出有理由“人为地”将其范围限制为 Callable objects(?)
  • @PiCTo 在语法上对装饰器来说可能无关紧要,但在语义上确实很重要,即根据装饰器的规范。这个装饰器只能与Callables 一起使用。 (这里的例子只是一个最小的例子。)

标签: python type-hinting python-typing


【解决方案1】:

可以参数化ProtocolCallable:

from typing import Callable, TypeVar, Protocol

C = TypeVar('C', bound=Callable)  # placeholder for any Callable


class CallableObj(Protocol[C]):   # Protocol is parameterised by Callable C ...
    attr1: str
    attr2: str

    __call__: C                   # ... which defines the signature of the protocol

这会创建Protocol 本身与任意Callable 的交集。


接受任何可调用C 的函数因此可以返回CallableObj[C],这是具有所需属性的相同签名的可调用:

def decorator(func: C) -> CallableObj[C]: ...

MyPy 正确识别签名和属性:

def dummy(arg: str) -> int: ...

reveal_type(decorator(dummy))           # CallableObj[def (arg: builtins.str) -> builtins.int]'
reveal_type(decorator(dummy)('Hello'))  # int
reveal_type(decorator(dummy).attr1)     # str
decorator(dummy)(b'Fail')  # error: Argument 1 to "dummy" has incompatible type "bytes"; expected "str"
decorator(dummy).attr3     # error: "CallableObj[Callable[[str], int]]" has no attribute "attr3"; maybe "attr2"?

【讨论】:

    【解决方案2】:

    由于typing.Callable 对应于collections.abc.Callable,您只需定义一个实现__call__ 的Protocol:

    class CallableWithAttrs(Protocol):
        attr1: str
        attr2: str
    
        def __call__(self, *args, **kwargs): pass
    

    【讨论】:

    • 我已经尝试继承其他 typing 协议,例如 Sized,而 mypy 没有抱怨。 Callable 是个例外有什么原因吗?
    • @Anakhand 请注意,Callable 在许多方面都是特殊情况。最重要的是,它是唯一一个接受 [[Args, ...], Ret] 的泛型,而其他所有 Generic 只接受 [Ts, ...]。
    • @Anakhand 我不完全确定,但我认为这是因为他们更喜欢使用callback protocols。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-09
    • 1970-01-01
    • 2021-04-18
    • 1970-01-01
    • 2016-04-14
    • 2015-07-06
    相关资源
    最近更新 更多