
我认为我们正在慢慢适应Python具有类型注释的事实:它们在功能和方法的注释( PEP 484 )中带回了两个版本(3.5),而在变量( PEP 526 )的最新版本(3.6)中又带了两个版本。
由于这两个PEP 均受MyPy的启发, 因此我将告诉您使用此静态分析器以及整个打字系统时,世俗的喜悦和认知失调在等待着我什么。
Disclamer:我没有提出在Python中使用静态类型的必要性或有害性的问题。 我只是在谈论我在静态类型的上下文中工作时遇到的陷阱。
泛型(打字通用)
在注解中使用类似List[int]
, Callable[[int, str], None]
东西很好。
分析器突出显示以下代码非常好:
T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'
但是,如果我们编写一个库,而使用它的程序员不会使用静态分析器,该怎么办?
强制用户使用值初始化类,然后存储其类型?
T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value)
不知何故不友好。
但是,如果您想这样做怎么办?
b = Gen[A](B())
在寻找这个问题的答案时,我经过一番typing
,然后跳入工厂的世界。

事实是,在初始化Generic类的实例之后,它将获得__origin_class__
属性,该属性具有__args__
属性,该属性是类型元组。 但是,不是从__init__
以及从__new__
。 而且它也不在__call__
元类中。 诀窍在于,在初始化Generic
的子类时Generic
它将变成另一个元类_GenericAlias
,它会在对象初始化后(包括其元类的所有方法)或在__getithem__
时设置最终类型。 因此,在构造对象时无法获得泛型类型。
我们抛出了这个垃圾,答应了更通用的解决方案。因此,我为自己写了一个解决这个问题的小描述符:
def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]
当然,因此,有必要重写以更广泛地使用它,但是本质很清楚。
[UPD]:早上,我决定尝试执行与typing
模块本身相同的操作,但更简单:
import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):
[UPD]: typing
开发人员Ivan Levinsky表示,这两种选择都可能无法预测。
无论如何,您可以使用任何方式。 也许__class_getitem__
甚至更好一些,至少__class_getitem__
是一个已记录的特殊方法(尽管它对泛型的行为不是)。
功能和别名
是的,泛型根本不容易:
例如,如果我们在某个地方接受一个函数作为参数,那么它的注解会自动从协变变为反变:
class A: pass class B(A): pass def foo(arg: 'A') -> None:
原则上,我对逻辑没有任何抱怨,只需通过通用别名即可解决:
TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:
通常,必须仔细阅读有关类型可变性的部分,并且应多次阅读。
向后兼容
并不是很热门:从3.7版起, Generic
是ABCMeta
的子类,非常方便且不错。 如果它在3.6上运行,则会破坏代码,这很不好。
结构继承(结构化原型)
刚开始我很高兴:接口已经交付了! 接口的作用是由类型类的Protocol
来执行的,该模块与@runtime
装饰器结合使用,可让您检查该类是否在没有直接继承的情况下实现了接口。 MyPy也将在更深层次上突出显示。
但是,与多重继承相比,我在运行时没有发现任何实际好处。
似乎装饰器仅检查具有所需名称的方法的存在,甚至不检查参数的数量,更不用说键入:
import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)
另一方面,MyPy则表现得更聪明,并突出了类型的不兼容性:
push_func(a, 1) push_func(b, 1)
操作员超载
一个非常新鲜的话题,因为 当给操作员提供全面的安全保护时,所有的乐趣就会消失。 这个问题在MyPy Bug跟踪器中屡次浮出水面,但在某些地方它仍然发誓,您可以放心地将其关闭。
我解释一下这种情况:
class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3
如果复合赋值方法返回NotImplemented
,Python首先搜索__radd__
,然后使用__add__
和__add__
。
重载任何形式的子类方法也是如此:
class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ...
在某些地方,警告已经转移到文档中,而在某些地方,警告仍然适用于产品。 但是,贡献者的一般结论是让这种超载可以接受。