在本课中,您将了解Python 3.8 中更精确的类型。 Python 的类型系统目前已经相当成熟。然而,在 Python 3.8 中,输入中添加了一些新功能,以允许更精确的输入:
Python支持可选类型提示,通常作为代码上的注释:
>>>>>> def double(number: float) -> float:
... return 2 * number
在这个例子中,你说number
应该是一个浮点数并且double()
也应该返回一个浮点数。然而,Python 将这些注释视为提示。它们不会在运行时强制执行:
>>>>>> double(3.14)
6.28
>>> double("I'm not a float")
"I'm not a floatI'm not a float"
double()
愉快地接受"I'm not a float"
作为一个参数,即使这不是一个浮点数。
类型提示允许静态类型检查器对 Python 代码进行类型检查,而无需实际运行脚本。这让人想起编译器捕获其他语言中的类型错误,例如爪哇, 和锈。此外,类型提示充当代码的文档,使其更易于阅读,并且改进 IDE 中的自动完成功能.
笔记:有几种可用的静态类型检查器,包括皮赖特, pytype, 和柴堆。在本课程中,您将使用迈皮。您可以从安装 Mypy皮伊使用pip:
$ python -m pip install mypy
在之前的代码示例中尝试 Mypy。创建一个新文件,名为float_check.py
:
# float_check.py
def double(number: float) -> float:
return 2 * number
double(3.14)
double("I'm not a float")
现在在此代码上运行 Mypy:
$ mypy float_check.py
float_check.py:8: error: Argument 1 to "double" has incompatible
type "str"; expected "float"
Found 1 error in 1 file (checked 1 source file)
根据类型提示,Mypy 能够告诉您第 8 行使用了错误的类型。将第二次调用的参数更改为double()
到一个float
:
# float_check.py
def double(number: float) -> float:
return 2 * number
double(3.14)
double(2.4)
现在再次在此代码上运行 Mypy:
$ mypy float_check.py
Success: no issues found in 1 source file
从某种意义上说,Mypy 是 Python 类型检查器的参考实现,并且正在被在 Dropbox 开发在 Jukka Lehtasalo 的领导下。 Python 的创建者 Guido van Rossum 是 Mypy 团队的一员。
您可以在 Python 中找到有关类型提示的更多信息原始 PEP 484,以及在Python 类型检查(指南),以及视频课程Python 类型检查.
有四个关于类型检查的新 PEP 已被接受并包含在 Python 3.8 中。您将看到每个示例的简短示例。
公众号 586介绍了文字类型。Literal
有点特殊,它代表一个或几个特定值。一个用例Literal
当字符串参数用于描述特定行为时,能够精确地添加类型。考虑以下示例:
# draw_line.py
def draw_line(direction: str) -> None:
if direction == "horizontal":
... # Draw horizontal line
elif direction == "vertical":
... # Draw vertical line
else:
raise ValueError(f"invalid direction {direction!r}")
draw_line("up")
该程序将通过静态类型检查器,即使"up"
是无效方向。类型检查器仅检查"up"
是一个字符串。在这种情况下,更准确地说,方向必须是文字字符串"horizontal"
或文字字符串"vertical"
。使用Literal
,你完全可以这样做:
# draw_line.py
from typing import Literal
def draw_line(direction: Literal["horizontal", "vertical"]) -> None:
if direction == "horizontal":
... # Draw horizontal line
elif direction == "vertical":
... # Draw vertical line
else:
raise ValueError(f"invalid direction {direction!r}")
draw_line("up")
通过向类型检查器公开允许的方向值,您现在可以收到有关错误的警告:
$ mypy draw_line.py
draw_line.py:15: error:
Argument 1 to "draw_line" has incompatible type "Literal['up']";
expected "Union[Literal['horizontal'], Literal['vertical']]"
Found 1 error in 1 file (checked 1 source file)
基本语法是Literal[<literal>]
。例如,Literal[38]
代表字面值38
。您可以使用以下方式表达多个文字值之一Union
:
Union[Literal["horizontal"], Literal["vertical"]]
由于这是一个相当常见的用例,因此您可以(并且可能应该)使用更简单的表示法Literal["horizontal", "vertical"]
反而。在添加类型时您已经使用了后者draw_line()
.
如果仔细查看上面 Mypy 的输出,您可以看到它在内部将更简单的表示法转换为 Union 表示法。
在某些情况下,函数返回值的类型取决于输入参数。一个例子是open()
,它可能返回一个文本字符串或一个字节数组,具体取决于mode
。这可以通过处理超载.
以下示例显示了一个计算器的框架,它可以将答案返回为常规数字(38
)或作为罗马数字 (XXXVIII
):
# calculator.py
from typing import Union
ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
def _convert_to_roman_numeral(number: int) -> str:
"""Convert number to a roman numeral string"""
result = list()
for arabic, roman in ARABIC_TO_ROMAN:
count, number = divmod(number, arabic)
result.append(roman * count)
return "".join(result)
def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
"""Add two numbers"""
result = num_1 + num_2
if to_roman:
return _convert_to_roman_numeral(result)
else:
return result
该代码具有正确的类型提示:结果add()
将是str
或者int
。但是,通常会使用文字来调用此代码True
或者False
作为价值to_roman
,在这种情况下,您希望类型检查器准确推断是否str
或者int
被返回。这可以使用以下方法完成Literal
和...一起@overload
:
# calculator.py
from typing import Literal, overload, Union
ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
def _convert_to_roman_numeral(number: int) -> str:
"""Convert number to a roman numeral string"""
result = list()
for arabic, roman in ARABIC_TO_ROMAN:
count, number = divmod(number, arabic)
result.append(roman * count)
return "".join(result)
@overload
def add(num_1: int, num_2: int, to_roman: Literal[True]) -> str: ...
@overload
def add(num_1: int, num_2: int, to_roman: Literal[False]) -> int: ...
def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
"""Add two numbers"""
result = num_1 + num_2
if to_roman:
return _convert_to_roman_numeral(result)
else:
return result
所添加的@overload
签名将帮助您的类型检查器推断str
或者int
取决于字面值to_roman
。请注意,省略号 (...
) 是代码的文字部分。它们代表重载签名中的函数体。
作为补充Literal
, 公众号 591介绍最终的。此限定符指定不应重新分配、重新定义或覆盖变量或属性。以下是打字错误:
# final_id.py
from typing import Final
ID: Final = 1
...
ID += 1
Mypy 将突出显示该行ID += 1
,并注意你Cannot
分配给final name "ID"
。这为您提供了一种确保代码中的常量永远不会改变其值的方法。
此外,还有一个@最终的可以应用于类和方法的装饰器。课程装饰的和@final
不能被子类化,同时@final
方法不能被子类覆盖:
# final_class.py
from typing import final
@final
class Base:
...
class Sub(Base):
...
Mypy 将使用错误消息标记此示例Cannot inherit from final class "Base"
。要了解更多信息Final
和@final
, 看公众号 591.
允许更具体类型提示的第三个 PEP 是公众号 589,其中介绍了类型字典。这可用于使用类似于类型化的表示法来指定字典中键和值的类型命名元组.
传统上,字典是用词典。问题是,这只允许一种类型的键和一种类型的值,通常会导致像这样的注释Dict[str, Any]
。例如,考虑一个注册有关 Python 版本信息的字典:
py38 = {"version": "3.8", "release_year": 2019}
对应的值version
是一个字符串,而release_year
是一个整数。这不能用精确表示Dict
。随着新TypedDict
,您可以执行以下操作:
# typed_dict.py
from typing import TypedDict
class PythonVersion(TypedDict):
version: str
release_year: int
py38 = PythonVersion(version="3.8", release_year=2019)
然后类型检查器将能够推断出py38["version"]
有类型str
, 尽管py38["release_year"]
是一个int
。在运行时,一个TypedDict
是一个常规的dict
,并且类型提示照常被忽略:
>>>>>> from typed_dict import *
>>> py38
{'version': '3.8', 'release_year': 2019}
>>> type(py38)
<class 'dict'>
如果您的任何值的类型错误,或者您使用尚未声明的键,Mypy 会让您知道。看公众号 589了解更多示例。
Mypy已经支持了协议已经有一段时间了。但是,那正式验收只发生在2019年5月。
协议是一种形式化 Python 对鸭子类型支持的方式:
当我看到一只鸟像鸭子一样走路、像鸭子一样游泳、像鸭子一样嘎嘎叫时,我称那只鸟为鸭子。 (来源)
例如,鸭子打字可以让你阅读.name
在任何具有.name
属性,而不真正关心对象的类型。对于打字系统来说支持这一点似乎是违反直觉的。通过结构子类型,仍然可以理解鸭子类型。
例如,您可以定义一个名为Named
可以识别所有带有a的对象.name
属性:
# protocol.py
from typing import Protocol
class Named(Protocol):
name: str
def greet(obj: Named) -> None:
print(f"Hi {obj.name}")
class Dog:
...
x = Dog()
greet(x)
创建后Named
协议,并定义功能greet()
这需要一个参数obj
,类型提示指定obj
遵循Named
协议。通过 Mypy 运行代码以查看发现的内容:
$ mypy protocol.py
protocol.py:16: error: Argument 1 to "greet" has incompatible type "Dog"; expected "Named"
Found 1 error in 1 file (checked 1 source file)
这Dog
类没有属性.name
,因此不符合检查要求Named
协议。添加一个.name
类的属性,带有默认字符串:
# protocol.py
from typing import Protocol
class Named(Protocol):
name: str
def greet(obj: Named) -> None:
print(f"Hi {obj.name}")
class Dog:
name = 'Good Dog'
x = Dog()
greet(x)
跑步protocol.py
再次通过Mypy:
$ mypy protocol.py
Success: no issues found in 1 source file
正如你所确认的,greet()
接受任何对象,只要它定义了一个.name
属性。看公众号 544和Mypy 文档有关协议的更多信息。