ホームページ  >  記事  >  バックエンド開発  >  Python 3 の型ヒントと静的分析

Python 3 の型ヒントと静的分析

WBOY
WBOYオリジナル
2023-09-03 20:21:101409ブラウズ

Python 3类型提示和静态分析

Python 3.5 では、新しい型モジュールが導入されており、関数注釈を使用してオプションの型ヒントを提供するための標準ライブラリ サポートが提供されます。これにより、静的型チェック (mypy など) や、将来的には自動化された型ベースの最適化への扉が開かれます。型ヒントは PEP-483 および PEP-484 で指定されています。

このチュートリアルでは、タイプヒントレンダリングの可能性を探り、mypy を使用して Python プログラムを静的に分析し、コードの品質を大幅に向上させる方法を示します。

入力ヒント

型ヒントは関数注釈の上に構築されます。つまり、関数アノテーションを使用すると、関数またはメソッドのパラメータと戻り値に任意のメタデータでアノテーションを付けることができます。型ヒントは、関数注釈の特殊なケースであり、関数パラメーターと戻り値に標準の型情報を特に注釈付けします。一般的な関数の注釈と特殊な型のヒントは完全にオプションです。簡単な例を見てみましょう:

リーリー

パラメータには、その型と戻り値の注釈が付けられます。ただし、Python はこれを完全に無視することを認識することが重要です。関数オブジェクトの アノテーション プロパティを通じて型情報を提供するだけで、それ以上の情報は提供しません。

リーリー

Python が本当に型ヒントを無視することを確認するために、型ヒントを完全にいじってみましょう:

リーリー

ご覧のとおり、コードは型ヒントに関係なく同じように動作します。

タイプヒントの動機

###わかりました。型ヒントはオプションです。 Python は型ヒントを完全に無視します。それで、彼らの言いたいことは何でしょうか?そうですね、いくつかの正当な理由があります:

    静的解析
  • IDEサポート
  • 標準ドキュメント
後で静的解析に Mypy を使用します。 IDE のサポートは、型ヒントの PyCharm 5 サポートから始まりました。標準ドキュメントは、関数のシグネチャを見るだけでパラメータと戻り値の型を簡単に見つけることができる開発者にとって役立ちます。また、ヒントから型情報を抽出できる自動ドキュメント ジェネレーターも備えています。

入力モジュール

入力モジュールには、型ヒントをサポートするように設計された型が含まれています。 int、str、list、dict などの既存の Python 型を使用しないのはなぜでしょうか?これらの型を使用することは間違いなく可能ですが、Python の動的型付けにより、基本的な型を超える情報はあまり得られません。たとえば、パラメーターを文字列と整数の間のマップにできるように指定したい場合、標準の Python 型を使用してこれを行うことはできません。入力モジュールを使用すると、次のように簡単になります:

リーリー

より完全な例、2 つのパラメーターを持つ関数を見てみましょう。そのうちの 1 つは辞書のリストで、各辞書には文字列キーと整数値が含まれています。もう 1 つのパラメータは文字列または整数です。型モジュールを使用すると、このような複雑なパラメータを正確に指定できます。

リーリー

便利なタイプ

型付けモジュールのさらに興味深い型をいくつか見てみましょう。

Python は関数を第一級市民として扱うため、Callable 型を使用すると、引数として渡すことも、結果として返すこともできる関数を指定できます。呼び出し可能オブジェクトの構文は、パラメーター型の配列 (これも型付けモジュールから) を提供し、その後に戻り値を提供します。わかりにくい場合は、次の例を参照してください:

リーリー

on_error コールバック関数は、Exception と整数を引数として受け取り、何も返さない関数として指定されます。

任意の型とは、静的型チェッカーがあらゆる操作と他の型への代入を許可する必要があることを意味します。すべての型は Any のサブタイプです。

先ほど説明した Union 型は、引数が複数の型になる可能性がある場合 (Python ではよくあることです) に便利です。次の例では、

verify_config() 関数は、Config オブジェクトまたはファイル名である構成パラメーターを受け入れます。ファイル名の場合は、別の関数を呼び出してファイルを解析して Config オブジェクトにし、それを返します。 リーリー

オプションのタイプは、パラメーターが None になることもできることを意味します。

Optional[T] Union[T, None] と同等

Iterable、Iterator、Reversible、SupportsInt、SupportsFloat、Sequence、MutableSequence、IO など、さまざまな関数を表すさらに多くの型があります。完全なリストについては、型付けモジュールのドキュメントを確認してください。

何よりも、引数の型を非常にきめ細かい方法で指定でき、Python 型システムを高い忠実度でサポートし、ジェネリックスと抽象基本クラスを使用できるようになります。

転送引用

場合によっては、メソッドの 1 つの型ヒントでクラスを参照したいことがあります。たとえば、クラス A が、A の別のインスタンスを取得してそれ自体とマージし、結果を返すマージ操作を実行できるとします。これは、タイプヒントを使用してそれを指定する素朴な試みです:

リーリー ###どうしたの? 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 已经利用它来提供更好的开发人员体验。

以上がPython 3 の型ヒントと静的分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。