卓越飞翔博客卓越飞翔博客

卓越飞翔 - 您值得收藏的技术分享站
技术文章17222本站已运行3325

Python 3类型提示和静态分析

Python 3类型提示和静态分析

Python 3.5 引入了新的类型模块,该模块为利用函数注释提供可选类型提示提供标准库支持。这为静态类型检查(如 mypy)以及未来可能的自动化基于类型的优化打开了大门。类型提示在 PEP-483 和 PEP-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 对象并返回。

'

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

'

print(A.merge.__annotations__)

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

annotations 属性仅按原样返回注释值。在本例中,它只是字符串“A”,而不是 A 类对象,“A”只是对其的前向引用。

'

print(get_type_hints(A.merge))

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

由于 None 默认参数,get_type_hints() 函数将 other 参数的类型转换为 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属性)。 p>

让我来说明所有这些概念。如果您尝试在使用常规字符串注释的函数上使用 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 函数注释对此进行了详细介绍。

'

@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() 也是安全的,并返回一个空字典。

'

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 已经利用它来提供更好的开发人员体验。

卓越飞翔博客
上一篇: 最小化需要删除的0的数量,以最大化最长连续1子串的长度
下一篇: 返回列表
留言与评论(共有 0 条评论)
   
验证码:
隐藏边栏