Python类型注释简介

引言



Magdalena Tomczyk的插图


第二部分


Python是一种具有动态类型的语言,它使我们能够自由地操纵不同类型的变量。 但是,在编写代码时,我们假设将使用哪种类型的变量(这可能是由于算法或业务逻辑的限制所致)。 对于程序的正确运行,对我们来说,重要的是要及早发现与错误类型的数据传输相关的错误。


在现代版本的Python(3.6+)中保留了动态类型化鸭子的想法,它支持变量类型,类字段,参数和函数的返回值的注释:



类型注释仅由Python解释器读取,不再处理,但可从第三方代码使用,并且主要设计用于静态分析器。


我叫Andrey Tikhonov,我在Lamoda从事后端开发。


在本文中,我想解释使用类型注释的基础,并考虑通过typing注释实现的典型示例。


注释工具


许多Python IDE都支持类型注释,这些注释会突出显示错误的代码或在您键入时提供提示。


例如,这是在Pycharm中的样子:


突出显示错误



温馨提示:



类型注释也由控制台linter处理。


这是pylint的输出:


 $ pylint example.py ************* Module example example.py:7:6: E1101: Instance of 'int' has no 'startswith' member (no-member) 

但是对于mypy找到的相同文件:


 $ mypy example.py example.py:7: error: "int" has no attribute "startswith" example.py:10: error: Unsupported operand types for // ("str" and "int") 

不同分析仪的行为可能有所不同。 例如,mypy和pycharm处理变量的类型不同。 在示例中,我将继续关注mypy的输出。


在某些示例中,代码可以在启动时无例外地运行,但是由于使用了错误类型的变量,可能包含逻辑错误。 在某些示例中,它甚至可能不会执行。


基础知识


与旧版本的Python不同,类型注释不是用注释或文档字符串编写的,而是直接在代码中编写的。 一方面,这破坏了向下兼容性,另一方面,这显然意味着它是代码的一部分,可以相应地进行处理。


在最简单的情况下,注释包含直接预期的类型。 下面将讨论更复杂的情况。 如果将基类指定为注释,则可以将其后代的实例作为值传递。 但是,您只能使用在基类中实现的那些功能。


变量的注释写在标识符后的冒号后面。 此后,可以初始化该值。 举个例子


 price: int = 5 title: str 

函数参数的注释方式与变量相同,返回值在箭头->和最后一个冒号之前表示。 举个例子


 def indent_right(s: str, width: int) -> str: return " " * (max(0, width - len(s))) + s 

对于类字段,定义类时必须显式指定注释。 但是,分析器可以基于__init__方法自动输出它们,但是在这种情况下,它们将在运行时不可用。 在本文的第二部分中阅读有关在运行时中使用注释的更多信息。


 class Book: title: str author: str def __init__(self, title: str, author: str) -> None: self.title = title self.author = author b: Book = Book(title='Fahrenheit 451', author='Bradbury') 

顺便说一句,在使用数据类时,必须在类中指定字段类型。 有关数据类的更多信息


内置类型


尽管您可以使用标准类型作为注释,但是在typing模块中隐藏了许多有用的东西。


选配


如果您将变量标记为int类型,然后尝试将其分配为None ,则会出现错误:


Incompatible types in assignment (expression has type "None", variable has type "int")


对于此类情况,在键入模块中提供了具有特定类型的Optional注释。 请注意,可选变量的类型在括号中指出。


 from typing import Optional amount: int amount = None # Incompatible types in assignment (expression has type "None", variable has type "int") price: Optional[int] price = None 

任何


有时您不想限制变量的可能类型。 例如,如果这确实不重要,或者您打算自己进行不同类型的处理。 在这种情况下,您可以使用Any注释。 Mypy不会发誓以下代码:


 unknown_item: Any = 1 print(unknown_item) print(unknown_item.startswith("hello")) print(unknown_item // 0) 

可能出现问题,为什么不使用object ? 但是,在这种情况下,假定尽管可以传输任何对象,但只能将其作为object的实例进行访问。


 unknown_object: object print(unknown_object) print(unknown_object.startswith("hello")) # error: "object" has no attribute "startswith" print(unknown_object // 0) # error: Unsupported operand types for // ("object" and "int") 

联盟


对于需要不仅允许使用任何类型,而且仅允许使用某些类型的情况,可以使用typing.Union批注,并在方括号中带有类型列表。


 def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100") # Argument 1 to "hundreds" has incompatible type "str"; expected "Union[int, float]" 

顺便说一句, Optional[T]注释等效于Union[T, None] ,尽管不建议这样的条目。


馆藏


类型注释机制支持通用机制( Generics ,在本文的第二部分中有更多介绍),该机制允许为容器指定存储在其中的元素的类型。


清单


为了指示变量包含列表,可以将列表类型用作批注。 但是,如果您要指定列表包含的元素,则此注释将不再适用。 这里有typing.Listtyping.List 。 与我们指定可选变量类型的方式类似,我们在方括号中指定列表项的类型。


 titles: List[str] = ["hello", "world"] titles.append(100500) # Argument 1 to "append" of "list" has incompatible type "int"; expected "str" titles = ["hello", 1] # List item 1 has incompatible type "int"; expected "str" items: List = ["hello", 1] 

假定列表包含无限数量的相同类型的元素。 但是对元素的注释没有任何限制:您可以使用AnyOptionalList和其他元素。 如果未指定项目类型,则假定为Any


除了列表之外,集合也有类似的注释: typing.FrozenSet 。集合和typing.FrozenSet


元组


与列表不同,元组通常用于异构元素。 语法相似,但有一个区别:方括号分别表示元组的每个元素的类型。


如果计划使用类似于该列表的元组:存储未知数量的相同类型的元素,则可以使用省略号( ... )。


不指定元素类型的注释Tuple工作方式类似于Tuple[Any, ...]


 price_container: Tuple[int] = (1,) price_container = ("hello") # Incompatible types in assignment (expression has type "str", variable has type "Tuple[int]") price_container = (1, 2) # Incompatible types in assignment (expression has type "Tuple[int, int]", variable has type "Tuple[int]") price_with_title: Tuple[int, str] = (1, "hello") prices: Tuple[int, ...] = (1, 2) prices = (1, ) prices = (1, "str") # Incompatible types in assignment (expression has type "Tuple[int, str]", variable has type "Tuple[int, ...]") something: Tuple = (1, 2, "hello") 

辞典


对于字典,使用typing.Dict 。 键类型和值类型分别注释:


 book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"} book_authors["1984"] = 0 # Incompatible types in assignment (expression has type "int", target has type "str") book_authors[1984] = "Orwell" # Invalid index type "int" for "Dict[str, str]"; expected type "str" 

类似使用的typing.DefaultDicttyping.OrderedDict


功能结果


您可以使用任何注释来指示函数结果的类型。 但是有一些特殊情况。


如果一个函数什么也不返回(例如,类似于print ),则其结果始终为None 。 对于注释,我们还使用None


结束该函数的有效选项是:显式返回None ,不指定值就返回,不调用return终止。


 def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return "" # No return value expected else: pass 

如果函数永不返回(例如,像sys.exit ),则应使用NoReturn批注:


 def forever() -> NoReturn: while True: pass 

如果它是一个生成器函数,即它的主体包含yield ,则可以对返回的函数使用Iterable[T]Generator[YT, ST, RT]批注:


 def generate_two() -> Iterable[int]: yield 1 yield "2" # Incompatible types in "yield" (actual type "str", expected type "int") 

而不是结论


在许多情况下,键入模块中都有合适的类型,但是我不会考虑所有内容,因为其行为与所考虑的行为相似。
例如,有一个Iterator作为collections.abc.Iterator的通用版本, typing.SupportsInt表示对象支持__int__方法,或者对支持__call__方法的函数和对象Callable


该标准还以注释和存根文件的形式定义了注释的格式,这些注释和存根文件仅包含静态分析器的信息。


在下一篇文章中,我将详细介绍泛型的工作机制以及在运行时中处理批注的机制。

Source: https://habr.com/ru/post/zh-CN432656/


All Articles