将任意函数与MyPy的TypeVar绑定属性一起使用

时间:2019-07-15 13:56:09

标签: python python-3.x generics mypy

我最近开始研究MyPy,并从他们的文档中看到了以下示例

from typing import TypeVar, SupportsAbs

T = TypeVar('T', bound=SupportsAbs[float])

def largest_in_absolute_value(*xs: T) -> T:
    return max(xs, key=abs)  # Okay, because T is a subtype of SupportsAbs[float].

哪个显示可以使用mypy,以便传入的泛型必须支持abs函数才能通过静态类型检查器。

但是我不清楚这是如何工作的。例如,如果我可以指定类型必须支持的任何功能,或者该类型必须介于两者之间的范围,那么我可以看到它非常强大。

我的问题如下:有没有一种方法可以使用绑定来支持任何随机函数要求?例如,类型必须支持len函数吗? (我怀疑这是可能的)

特定变量类型的范围(即小于10个字符的字符串或小于100的整数)如何? (我怀疑这不太可能)

1 个答案:

答案 0 :(得分:3)

核心原则是:绑定必须是某种合法的PEP-484类型。

通常,所有这些操作都是让您指定T必须最终由边界或边界的某些子类“填充”。例如:

class Parent: pass
class Child(Parent): pass

T = TypeVar('T', bound=Parent)

def foo(x: T) -> T: return x

# Legal; revealed type is 'Parent'
reveal_type(foo(Parent()))  

# Legal; revealed type is 'Child'
reveal_type(foo(Child()))

# Illegal, since ints are not subtypes of Parent
foo(3)

通过将绑定设为Protocol,您可以做一些更有趣的事情。

基本上,假设您有一个像这样的程序:

class SupportsFoo:
    def foo(self, x: int) -> str: ...

class Blah:
    def foo(self, x: int) -> str: ...

# The two types are not related, so this fails with a
# 'Incompatible types in assignment' error -- the RHS needs
# to be a subtype of the declared type of the LHS.
x: SupportsFoo = Blah()

mypy认为这两个类是完全不相关的:它们可能都碰巧共享具有相同签名的函数foo,但是Blah不会继承自SupportsFoo或反之亦然,因此将它们的相似性视为巧合,因此将其丢弃。

我们可以通过将SupportsFoo变成协议

# If you're using Python 3.7 or below, pip-install typing_extensions
# and import Protocol from there
from typing import Protocol

class SupportsFoo(Protocol):
    def foo(self, x: int) -> str: ...

class Blah:
    def foo(self, x: int) -> str: ...

# This succeeds!
x: SupportsFoo = Blah()

现在,成功了! Mypy知道Blah具有与SupportsFoo完全相同的签名的方法,因此将其视为前者的子类型。


这正是SupportsAbs发生的事情-您可以在Typeshed(标准库的类型提示存储库)上检查definition of that type for yourself。 (将Typeshed的副本包含在每个mypy版本中):

@runtime_checkable
class SupportsAbs(Protocol[_T_co]):
    @abstractmethod
    def __abs__(self) -> _T_co: ...

是的,按照您的要求,您还可以创建一个协议,以坚持使用__len__的输入类型实现typing.Sized,其定义如下:

@runtime_checkable
class Sized(Protocol, metaclass=ABCMeta):
    @abstractmethod
    def __len__(self) -> int: ...

是的,您的直觉是,没有一种明确的方法来创建类型来断言诸如“此字符串必须小于等于10个字符”或“此整数必须小于等于100个字符”之类的事情。

我们可以通过以下类似的方式使用称为Literal types的无关机制来为这种攻击提供支持:

# As before, import from typing_extensions for Python 3.7 or less
from typing import Literal

BetweenZeroAndOneHundred = Literal[
    0, 1, 2, 3, 4, 5,
    # ...snip...
    96, 97, 98, 99, 100,
]

但是,这是很棘手的,实际上讲它的价值非常有限。

更好的解决方案是通常只在运行时进行自定义检查并使用NewType

from typing import NewType

LessThanOneHundred = NewType('LessThanOneHundred', int)

def to_less_than_one_hundred(value: int) -> LessThanOneHundred:
    assert value < 100
    return LessThanOneHundred(value)

这不是一个完美的解决方案,因为它要求您在运行时进行检查/要求您确保在完成运行时检查后仅能“实例化”您的NewType,但这实际上是 以类型检查器可以理解的形式对任意运行时检查结果进行编码的一种可行方法。