首頁  >  文章  >  後端開發  >  Python 3型別提示與靜態分析

Python 3型別提示與靜態分析

WBOY
WBOY原創
2023-09-03 20:21:101453瀏覽

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

以上是Python 3型別提示與靜態分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn