我认为静态类型似乎被吹捧过高了。
尽管如此,mypy极低的侵入性能带来许多好处。关于如何在现有的Python项目中添加类型,以下是我的一些想法,大致按重要性排序。
首先确保mypy成功运行
Mypy上手时两个很常见的问题有:
1.Mypy没有作为构建的一部分运行
2.mypy 虽然正在运行,但它没有找到任何源文件或只找到了一部分源文件
Mypy的“默认允许”特性使两者都极易出现。无论出现哪种情况,都会非常痛苦,因为最后人们应用的类型,其实并未被检查,因而问题会慢慢暴露出来,让人非常困惑。
要手动添加类型的地方
Mypy 可进行类型推断,即通过检查有关值的代码,基于上下文区分值的类型。但事实上,由于mypy“渐进式推断”的特点,(如果不确定,它将推断成Any类型),比起像Haskell等其他推断性的语言,在python中更需要手动提供类型。
我目前的想法是:你应该力求为所有函数实参和返回值1 (以及其他任何mypy需要帮助的地方)提供类型。
一般来说,变量不需要应用类型,尽管这可能有助于使你的代码更清晰,或者帮助处理你不理解的类型错误。
Opitional会被频繁使用
在实践中最重要的一种类型就是 Optional. Optional 被用于可为空的值. 举个例子:
大量的代码会使用Optional配合其他类型使用。有了Optional,mypy就能够检查空值,无论该值被用在哪里。
Optional是一种简单的类型但它却能够查出大量的缺陷,它可能是整个类型检查体系中最好的部分。
考虑是否要包含你的测试
关于是否在类型检查中包含测试(即也对测试进行类型检查),我不知道有没有统一答案。有些项目确实能在 tests/ 目录上运行mypy,有些则不能。
在类型检查2中包含测试的主要优点是:你能迅速发现应用类型和预期用法不一致,或mypy推断的类型与预期用法不符。
这在新的或没有太多类型应用的代码库中尤其有用。另外,测试也能用来改善IDE的 tab-completion
然而不利的一面就是,通常出于Mock(模拟)和Fake(伪造)的目的,测试有时会对你的代码进行一些古怪操作,而且,某些测试模式通过类型检查器可能有点困难 这虽然不是什么大问题,但似乎有点浪费时间,并且会削弱类型的优势。
有选择性地使用第三方stub(存根)
一些库包含大量运行时的元编程技巧. 因而这些库通常不会提供太多类型信息。
在程序中心使用了这些库,却不能获取类型信息,这会十分恼人。一些库有第三方Stub文件,例如sqlalchemy有sqlalchemy-stubs ,它会提供一些有用但不完全的类型。
这些库并非都有有用的第三方Stub。在撰写本文时,我对boto的任何第三方Stub都不信服。最好的方法似乎是在调用AWS / Openstack API时学会与Any一起使用(但要用moto进行彻底测试)。
偶尔需要应急措施
你偶尔会遇到这样的情况:代码正确但mypy无法分辨。这里有几种解决方法:
第一种(可能是最好的方法)就是用typing.cast,它会告诉 mypy 你知道的比它知道的更多。这将在整个代码中保持类型检查,除非通知 mypy做一个特定的更正。
第二种选择就是值显式设置为Any。这将禁用检查该特定值。如果所讨论的值是没有简单类型的复杂对象,则可以使用此方法。
第三种就是使用 # type: ignore 语法. 如果问题不在于确定特定类型,而在于mypy误认为被破坏了的某些不变类型,用这个会很方便。
考虑加一个注释去解释原因
优先选择一些严格性选项
Mypy的 mypy.ini 文件允许进行广泛的配置。我还没见过哪个实操项目避免使用mypy配置的。以下是我的首选:
check_untyped_defs 使mypy尝试检查没有类型注释的函数内部。否则,对于类型注释级别较低的项目mypy很少检查。
no_implicit_optional 当你设定参数不是空值,但实际上它们可为空时,它将提示类型错误。
ignore_missing_imports 这个应谨慎使用,不要将其应用于整个mypy 中,否则会极大地增加由于错误导致重要代码检查失败的风险。用此配置选项来标记特定模块(或名称空间)即可。
一个可行的例子:
一旦你的项目深度耦合了mypy,我建议最好去查看mypy提供的所有其他严格选项。从低严格度级别开始,朝着高严格度级别推进是一个好的策略。
如何调试类型问题
对于难懂的类型问题,这里给出两个策略来调试。
第一种:你可以将类型应用于出错类型周围,如变量、函数参数、循环迭代变量等。这样有助于将错误从难懂的那行代码中移动到更容易看出问题的地方。
第二:使用魔法般的mypy内置函数。reveal_type(expr)能使 mypy 打印出给定表达式类型的意见;reveal_locals() 则会让mypy打印出范围内所有变量的类型. 这两者中,我用 reveal_locals() 更多一些。
抽象,具体,可变和不变
Mypy 有具体的类型,如 List 和 Dict;也有抽象的类型,如 Sequence 和Mapping
一些抽象的类型还分可变和不可变的版本,例如Set 和 MutableSet ,Mapping 和 MutableMapping
因此我们需要去选择应用哪种类型及何时应用。
我的朋友Oli Russell提出以下策略:
让参数类型尽可能抽象
这样能使调用者尽可能自由地传递他们想要的
2.让返回类型(更)具体
同样也是为了让调用者尽可能自由地使用返回值
以上与我的经验相符。如果你过于苛刻, 比如,你返回 Sequence 而不是 List, 那么别人之后还要去编辑你的返回类型才能达到他们的目的。
同样的, 太具体的参数类型也很麻烦。你会发现无法将自定义的类似dict的对象传递给函数。有的对象只定义了__getitem__,但它确实可以当成一个Dict来使用。
你可以查阅 collections.abc 的标准库文档,去找每种抽象类型中包含的方法。以下是最常用的:
Iterable
Sequence 和 MutableSequence
Mapping 和 MutableMapping
Set 和 MutableSet
另外还有其他考虑。也许你想要阻止调用者修改你要返回的东西 (可能是因为你正在内部使用它3)。在这种情况下,最好返回 Mapping 而不是Dict。
Typed dataclasses(类型化的数据类)
Python 3.7 引入了 dataclasses. 下面是一个可能的类定义,如果你的类主要包含数据 (而不是行为)。它能为你提供一个更简洁的语法。
类型系统也支持这样的类,来作为其值的类型。
但缺点就是,大量更改其数据表示形式的代码往往不是快速代码。如果一个不可变的 Mapping作用于程序中的大部分,那它会比有一系列中间数据类要更快。
TypedDict
在 mypy 的扩展库中还有一个typed dictionary 。自3.8版本起它在标准库里。
TypedDict 能让你通过类型系统来控制存在哪些键以及它们的键值是什么,这一点胜过平常用的Mapping[str, str]。
它能成为Typed dataclasses的一个有效替代方案,且通常速度更快。
泛型和类型变量
mypy包括对类型变量和泛型类型的支持. 似乎大多数人发现如List等使用起来简单自然,但当有新的类型被创建出来之后,代码往往倾向于将类型限制定义为基类。
但是不是所有的泛型都不符合规则 , 在各式各样的Python 代码中,泛型偶尔也有使用价值。
泛型允许变量的类型更灵活 ,但它会保持检查 ,举个例子:
另一个例子:
泛型能让你定义专门作用于某种类型变量的类。下面这个例子中,我们将D绑定到一个特定的抽象基类。
我相信接下来几年这个将被广泛使用
ABCs vs Protocols
有时,需要将抽象类型应用于具有各种具体选项的事物(此处在实践中和使用泛型类型有交叉)。有两种方法可以做到这一点。
第一种:通过下面的方法命名父基类,子类将从该基类继承。
这里的 feed_animal 标记为采用Animal (一种抽象基类)。
第一种方法称为名义子类型化,作为在面向对象的语言中使用抽象类型的传统方式,大多数人应该熟悉。
还有第二种(相对于Python)较新的方法,在该方法中,您无需命名父类,而是命名一个有你所需方法的“协议”。
这里的feed_animal 被标记使用 Carnivore (一种协议)。注意,我不必将任何animal类标记为成员:如果它们具有相同类型的eat_meat方法,则它们将自动作为Carnivore的一部分
协议是“开放的”,因此任何具有匹配eat_meat方法的类都将被算作成员。这种方法叫做结构子类型化。有许多 built in protocols(内置协议),例如Sized,SupportsBytes,Container等。
如果你能控制足够多的类层次结构,则可以选择名义子类型,但是如果不能,则必须使用结构子类型。
与泛型类型一样,这个最好谨慎使用。希望绝大多数情况下,具体类型就够用了,这样就不必在代码库中填充大量协议和抽象基类了。
最后的提示
一些人太过于执迷类型了,我目睹了大量Haskell社区的人一头扎进自己的创作中无法自拔,变得痴痴傻傻,实在有点看不下去。
要小心变成类型狂!不要忽略这样一个事实:类型检查是修正错误的一种辅助,而勿满足于类型检查本身。
另请参阅
Dropbox是最早将mypy应用于其代码库的公司之一。对此他们也写了经验,读来十分有趣。"The Tangle"在许多Python项目中出现过。
注释:
强制执行此操作的相关配置选项是disallow_untyped_defs,但对现有项目立即启用它通常只会贪多嚼不烂。
请注意,你必须在与主代码库相同的mypy中运行测试。将测试分开各自运行是得不到任何好处的。
正如我在本文提到过的,避免在任何地方创建新集合的一个很好的理由是速度。用不可变的抽象类型标记返回类型有助于对此进行静态分析。
英文原文:http://calpaterson.com/mypy-hints.html
译者:ᐛ