引言

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
任何
有时您不想限制变量的可能类型。 例如,如果这确实不重要,或者您打算自己进行不同类型的处理。 在这种情况下,您可以使用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"))
联盟
对于需要不仅允许使用任何类型,而且仅允许使用某些类型的情况,可以使用typing.Union
批注,并在方括号中带有类型列表。
def hundreds(x: Union[int, float]) -> int: return (int(x) // 100) % 10 hundreds(100.0) hundreds(100) hundreds("100")
顺便说一句, Optional[T]
注释等效于Union[T, None]
,尽管不建议这样的条目。
馆藏
类型注释机制支持通用机制( Generics ,在本文的第二部分中有更多介绍),该机制允许为容器指定存储在其中的元素的类型。
清单
为了指示变量包含列表,可以将列表类型用作批注。 但是,如果您要指定列表包含的元素,则此注释将不再适用。 这里有typing.List
。 typing.List
。 与我们指定可选变量类型的方式类似,我们在方括号中指定列表项的类型。
titles: List[str] = ["hello", "world"] titles.append(100500)
假定列表包含无限数量的相同类型的元素。 但是对元素的注释没有任何限制:您可以使用Any
, Optional
, List
和其他元素。 如果未指定项目类型,则假定为Any
。
除了列表之外,集合也有类似的注释: typing.FrozenSet
。集合和typing.FrozenSet
。
元组
与列表不同,元组通常用于异构元素。 语法相似,但有一个区别:方括号分别表示元组的每个元素的类型。
如果计划使用类似于该列表的元组:存储未知数量的相同类型的元素,则可以使用省略号( ...
)。
不指定元素类型的注释Tuple
工作方式类似于Tuple[Any, ...]
price_container: Tuple[int] = (1,) price_container = ("hello")
辞典
对于字典,使用typing.Dict
。 键类型和值类型分别注释:
book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"} book_authors["1984"] = 0
类似使用的typing.DefaultDict
和typing.OrderedDict
功能结果
您可以使用任何注释来指示函数结果的类型。 但是有一些特殊情况。
如果一个函数什么也不返回(例如,类似于print
),则其结果始终为None
。 对于注释,我们还使用None
。
结束该函数的有效选项是:显式返回None
,不指定值就返回,不调用return
终止。
def nothing(a: int) -> None: if a == 1: return elif a == 2: return None elif a == 3: return ""
如果函数永不返回(例如,像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"
而不是结论
在许多情况下,键入模块中都有合适的类型,但是我不会考虑所有内容,因为其行为与所考虑的行为相似。
例如,有一个Iterator
作为collections.abc.Iterator
的通用版本, typing.SupportsInt
表示对象支持__int__
方法,或者对支持__call__
方法的函数和对象Callable
。
该标准还以注释和存根文件的形式定义了注释的格式,这些注释和存根文件仅包含静态分析器的信息。
在下一篇文章中,我将详细介绍泛型的工作机制以及在运行时中处理批注的机制。