Python 3类型提示和静态分析

Python 3.5引入了新的类型模块,该模块提供标准库支持,以利用功能注释来标注可选的类型提示。 这就为诸如mypy之类的静态类型检查打开了新的有趣的工具之门,并在将来可能会自动进行基于类型的优化。 类型提示在PEP-483PEP-484中指定。

在本教程中,我将探讨出现类型提示的可能性,并向您展示如何使用mypy静态分析Python程序并显着提高代码质量。

类型提示

类型提示建立在功能注释的基础上。 简要地说,函数注释使您可以使用任意元数据来注释函数或方法的参数和返回值。 类型提示是函数注释的一种特殊情况,它专门用标准类型信息注释函数参数和返回值。 通常,功能注释和特别是类型提示是完全可选的。 让我们看一个简单的例子:

def reverse_slice(text: str, start: int, end: int) -> str:
    return text[start:end][::-1]
    
reverse_slice('abcdef', 3, 5)
'ed'

参数使用其类型以及返回值进行注释。 但至关重要的是要意识到Python完全忽略了这一点。 它通过函数对象的注释属性使类型信息可用,仅此而已。

reverse_slice.__annotations
{'end': int, 'return': str, 'start': int, 'text': str}

为了验证Python确实忽略了类型提示,让我们完全弄乱类型提示:

def reverse_slice(text: float, start: str, end: bool) -> dict:
    return text[start:end][::-1]
    
reverse_slice('abcdef', 3, 5)
'ed'

如您所见,无论类型提示如何,代码的行为都相同。

类型提示的动机

好。 类型提示是可选的。 Python完全忽略类型提示。 那他们有什么意义呢? 好吧,有几个很好的理由:

  • 静态分析
  • IDE支持
  • 标准文件

稍后,我将与Mypy一起进行静态分析。 IDE支持已经从PyCharm 5对类型提示的支持开始。 标准文档非常适合开发人员,他们只需查看函数签名即可轻松确定参数的类型和返回值,以及可以从提示中提取类型信息的自动文档生成器。

typing模块

键入模块包含旨在支持类型提示的类型。 为什么不只使用现有的Python类型,例如int,str,list和dict? 您绝对可以使用这些类型,但是由于Python的动态类型,除了基本类型之外,您没有太多的信息。 例如,如果要指定参数可以是字符串和整数之间的映射,则无法使用标准Python类型来实现。 使用键入模块,它就像:

Mapping[str, int]

让我们看一个更完整的示例:一个带有两个参数的函数。 其中之一是字典列表,其中每个字典都包含字符串(字符串)和值(整数)。 另一个参数是字符串或整数。 输入模块允许对此类复杂参数的精确说明。

from typing import List, Dict, Union

def foo(a: List[Dict[str, int]],
        b: Union[str, int]) -> int:
    """Print a list of dictionaries and return the number of dictionaries
    """
    if isinstance(b, str):
        b = int(b)
    for i in range(b):
        print(a)


x = [dict(a=1, b=2), dict(c=3, d=4)]
foo(x, '3')

[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}]
[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}]
[{'b': 2, 'a': 1}, {'d': 4, 'c': 3}]

有用的类型

让我们从打字模块中看到一些更有趣的类型。

Callable类型允许您指定可以作为参数传递或作为结果返回的函数,因为Python将函数视为一等公民。 可调用函数的语法是提供一个参数类型数组(同样来自键入模块),后跟一个返回值。 如果这令人困惑,请举一个例子:

def do_something_fancy(data: Set[float], on_error: Callable[[Exception, int], None]):
    ...

将on_error回调函数指定为以Exception和一个整数作为参数且不返回任何值的函数。

Any类型意味着静态类型检查器应允许任何操作以及对任何其他类型的分配。 每个类型都是Any的子类型。

当参数可以具有多种类型时,您前面看到的Union类型很有用,这在Python中很常见。 在以下示例中, verify_config()函数接受config参数,该参数可以是Config对象或文件名。 如果是文件名,它将调用另一个函数将文件解析为Config对象并返回它。

def verify_config(config: Union[str, Config]):
    if isinstance(config, str):
        config = parse_config_file(config)
    ...
    
def parse_config_file(filename: str) -> Config:
    ...

Optional类型表示该参数也可以为None。 Optional[T]等效于Union[T, None]

还有更多类型表示各种功能,例如Iterable,Iterator,Reversible,SupportsInt,SupportsFloat,Sequence,MutableSequence和IO。 查看打字模块文档以获取完整列表。

最主要的是,您可以以非常细粒度的方式指定参数的类型,以高度保真地支持Python类型系统,并且也允许泛型和抽象基类。

前向参考

有时您想在一个方法的类型提示中引用一个类。 例如,假设类A可以执行某些合并操作,该合并操作采用A的另一个实例,并与自身合并并返回结果。 这是天真的尝试使用类型提示来指定它:

class A:
    def merge(other: A) -> A:
        ...

      1 class A:
----> 2         def merge(other: A = None) -> A:
      3                 ...
      4

NameError: name 'A' is not defined

发生了什么? 当Python检查其merge()方法的类型提示时,尚未定义类A,因此此时(直接)不能使用类A。 该解决方案非常简单,并且我之前已经看过SQLAlchemy使用过该解决方案。 您只需将类型提示指定为字符串。 Python将理解这是一个前向参考,并会做正确的事情:

class A:
    def merge(other: 'A' = None) -> 'A':
        ...

类型别名

将类型提示用于长类型规范的一个缺点是,即使它提供了很多类型信息,也可能使代码混乱并使代码可读性降低。 您可以像其他任何对象一样对类型进行别名。 就像这样简单:

Data = Dict[int, Sequence[Dict[str, Optional[List[float]]]]

def foo(data: Data) -> bool:
    ...

get_type_hints()辅助函数

键入模块提供get_type_hints()函数,该函数提供有关参数类型和返回值的信息。 尽管注解属性返回类型提示是因为它们只是注解,但我仍然建议您使用get_type_hints()函数,因为它可以解析前向引用。 另外,如果您为其中一个参数指定默认值None,则get_type_hints()函数将自动返回其类型为Union [T,NoneType](如果您刚刚指定了T)。让我们看看使用A.merge()方法的区别先前定义:

print(A.merge.__annotations__)

{'other': 'A', 'return': 'A'}

注解属性仅按原样返回注解值。 在这种情况下,它只是字符串“ A”,而不是A类对象,“ A”只是对它的前向引用。

print(get_type_hints(A.merge))

{'return': <class '__main__.A'>, 'other': typing.Union[__main__.A, NoneType]}

由于默认参数为None,get_type_hints()函数将另一个参数的类型转换为A(类)和NoneType的并集。 返回类型也转换为A类。

装饰者

类型提示是功能注释的一种特殊形式,它们也可以与其他功能注释并排使用。

为此,键入模块提供了两个修饰符: @no_type_check@no_type_check_decorator@no_type_check装饰器可以应用于类或函数。 它将no_type_check属性添加到函数(或类的每个方法)。 这样,类型检查器将知道忽略不是类型提示的注释。

这有点麻烦,因为如果您编写将广泛使用的库,则必须假定将使用类型检查器,并且如果要用非类型提示注释函数,还必须使用@no_type_check装饰它们。

使用常规功能注释时,常见的情况还包括在其上进行操作的装饰器。 在这种情况下,您还想关闭类型检查。 一种选择是除了使用装饰器之外,还使用@no_type_check装饰器,但是这样会变老。 相反,可以使用@no_Type_check_decorator装饰您的装饰器,使其也像@no_type_check (添加no_type_check属性)一样工作。

让我说明所有这些概念。 如果您尝试在使用常规字符串注释进行注释的函数上使用get_type_hint()(就像任何类型检查器一样),则get_type_hints()会将其解释为正向引用:

def f(a: 'some annotation'):
    pass

print(get_type_hints(f))

SyntaxError: ForwardRef must be an expression -- got 'some annotation'

为了避免这种情况,请添加@no_type_check装饰器,get_type_hints仅返回一个空字典,而__annotations__属性返回注释:

@no_type_check
def f(a: 'some annotation'):
    pass
    
print(get_type_hints(f))
{}

print(f.__annotations__)
{'a': 'some annotation'}

现在,假设我们有一个装饰器来打印注释字典。 您可以使用@no_Type_check_decorator装饰它,然后装饰该函数,而不必担心某些类型检查器调用get_type_hints()并感到困惑。 对于可能对注释进行操作的每个装饰器,这可能都是最佳实践。 不要忘记@ functools.wraps ,否则注释将不会复制到装饰函数中,并且所有内容都会崩溃。 这在Python 3 Function Annotations中有详细介绍。

@no_type_check_decorator
def print_annotations(f):
    @functools.wraps(f)
    def decorated(*args, **kwargs):
        print(f.__annotations__)
        return f(*args, **kwargs)
    return decorated

现在,您可以仅使用@print_annotations装饰该函数,并且在调用该函数时将打印其注释。

@print_annotations
def f(a: 'some annotation'):
    pass
    
f(4)
{'a': 'some annotation'}

调用get_type_hints()也是安全的,并返回一个空dict。

print(get_type_hints(f))
{}

用Mypy进行静态分析

Mypy是一个静态类型检查器,它是类型提示和键入模块的灵感来源。 Guido van Rossum本人是PEP-483的作者,也是PEP-484的合著者。

安装Mypy

Mypy处于非常活跃的开发中,截至撰写本文时,PyPI上的软件包已过时且不适用于Python 3.5。 要将Mypy与Python 3.5结合使用,请从GitHub上Mypy的存储库中获取最新版本 。 就像这样简单:

pip3 install git+git://github.com/JukkaL/mypy.git

和Mypy一起玩

一旦安装了Mypy,就可以在程序上运行Mypy。 以下程序定义了一个期望字符串列表的函数。 然后,它使用整数列表调用该函数。

from typing import List

def case_insensitive_dedupe(data: List[str]):
    """Converts all values to lowercase and removes duplicates"""
    return list(set(x.lower() for x in data))


print(case_insensitive_dedupe([1, 2]))

运行该程序时,它显然在运行时失败,并出现以下错误:

python3 dedupe.py
Traceback (most recent call last):
  File "dedupe.py", line 8, in <module>
    print(case_insensitive_dedupe([1, 2, 3]))
  File "dedupe.py", line 5, in case_insensitive_dedupe
    return list(set(x.lower() for x in data))
  File "dedupe.py", line 5, in <genexpr>
    return list(set(x.lower() for x in data))
AttributeError: 'int' object has no attribute 'lower'

这是什么问题? 问题在于,即使在这种非常简单的情况下,也无法立即得知根本原因是什么。 输入类型有问题吗? 也许代码本身是错误的,不应尝试在“ int”对象上调用lower()方法。 另一个问题是,如果您没有100%的测试覆盖率(老实说,我们当中没有一个人),那么这些问题可能会在一些未经测试,很少使用的代码路径中潜伏,并在生产中最差的时间被发现。

通过确保始终使用正确的类型调用函数(带有类型提示的注释),在类型提示的辅助下,静态类型为您提供了额外的安全网。 这是Mypy的输出:

(N) > mypy dedupe.py
dedupe.py:8: error: List item 0 has incompatible type "int"
dedupe.py:8: error: List item 1 has incompatible type "int"
dedupe.py:8: error: List item 2 has incompatible type "int"

这很简单,直接指出问题,不需要运行大量测试。 静态类型检查的另一个好处是,如果您提交它,则可以跳过动态类型检查,除非解析外部输入(读取文件,传入的网络请求或用户输入)。 就重构而言,它也建立了很多信心。

结论

类型提示和键入模块是Python表现力的完全可选的补充。 尽管它们可能不适合每个人的口味,但是对于大型项目和大型团队而言,它们可能是必不可少的。 有证据表明,大型团队已经在使用静态类型检查。 现在,类型信息已经标准化,共享使用它的代码,实用程序和工具将变得更加容易。 像PyCharm这样的IDE已经利用它来提供更好的开发人员体验。

翻译自: https://code.tutsplus.com/tutorials/python-3-type-hints-and-static-analysis--cms-25731

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值