Python 中的简单依赖类型
最初发表在我的博客:https://sobolevn.me/2019/01/simple-dependent-types-in-python
我对这个新功能感到非常兴奋python
:简单依赖类型。
“依赖类型”听起来可能很复杂,但其实不然。相反,它是一个非常有用的功能,我将展示它的工作原理以及何时应该依赖它。
我们不会深入探讨这个理论,也不会提供任何数学公式。正如史蒂芬·霍金曾经说过的:
有人告诉我,我在书里加的每个方程式都会使销量减半。因此,我决定完全不加任何方程式。不过,最终我还是加了一个方程式,那就是爱因斯坦著名的方程式 E=mc^2。我希望这不会吓跑一半的潜在读者。
什么是依赖类型?它指的是你依赖某些类型的值,而不仅仅是原始类型。
考虑这个例子:
from typing import Union
def return_int_or_str(flag: bool) -> Union[str, int]:
if flag:
return 'I am a string!'
return 0
我们可以清楚地看到,根据 的值,flag
我们可以得到str
或int
的值。结果类型将是Union[str, int]
。每次我们调用这个带有混合返回类型的函数时,我们都必须检查实际得到的类型以及如何处理它。这很不方便,并且会使你的代码更加复杂。
你可能会说这个函数很糟糕,它不应该像现在这样运行。没错,但有些实际用例的设计就是如此。
考虑一下open
标准库中的函数。你有多少次因为混淆了str
和 而导致运行时错误bytes
?我遇到过无数次。我可不想再发生这种事了!所以,这次我们要编写类型安全的代码。
def open_file(filename: str, mode: str):
return open(filename, mode)
我们期望这里的返回类型是什么?str
等一下!我们可以这样调用它:open_file('some.txt', 'rb')
它会返回bytes
!所以,返回类型是Union[IO[str], IO[bytes]]
。处理它真的很难。我们最终会得到很多条件、不必要的强制类型转换和保护。
依赖类型解决了这个问题。但是,在继续之前,我们必须了解一些稍后会用到的原语。
文字和@overload
如果您没有mypy
安装typing_extensions
,则需要安装这些软件包的最新版本。
» pip install mypy typing_extensions
现在我们准备利用Literal
和的功能重写我们的代码@overload
:
from typing import overload
from typing_extension import Literal
补充说明一下:typing
是一个内置python
模块,其中定义了所有可能的类型。并且该模块的开发速度受限于新python
版本的发布。 并且typing_extensions
是一个官方软件包,用于包含未来版本中将提供的新类型python
。因此,它确实解决了常规模块发布速度和频率的所有问题typing
。
文字
Literal
type 表示特定类型的特定值。
from typing_extensions import Literal
def function(x: Literal[1]) -> Literal[1]:
return x
function(1)
# => OK!
function(2)
# => Argument has incompatible type "Literal[2]"; expected "Literal[1]"
要运行此代码,请使用:mypy --python-version=3.6 --strict test.py
。本文中的所有示例都将保持不变。
Literal[0]
太棒了!但是,和类型之间有什么区别呢int
?
from typing_extensions import Literal
def function(x: int = 0, y: Literal[0] = 0) -> int:
reveal_type(x)
# => Revealed type is 'builtins.int'
reveal_type(y)
# => Revealed type is 'Literal[0]'
return x
显示的类型有所不同。获取Literal
类型的唯一方法是将 注释为Literal
。这样做是为了保持与 的旧版本的向后兼容性mypy
,而不是将其检测x: int = 0
为Literal
类型。因为 的值x
以后可能会更改。
您可以在可以使用Literal[0]
常规的任何地方int
使用它,但反之则不行。
from typing_extensions import Literal
def function(x: int, y: Literal[0]) -> int:
return x
x1: int = 0
y1: Literal[0] = 0
function(y1, y1)
function(x1, x1)
# => Argument 2 has incompatible type "int"; expected "Literal[0]"
看到了吗?因为x1
是一个变量——它不能在我们期望的位置使用Literal
。
在本系列的第一部分中,我写了一篇关于在 中使用实数常量python
的文章。如果你不知道 中变量和常量的区别,可以读一读python
。
在这种情况下,常数会有帮助吗?是的,会的!
from typing_extensions import Literal, Final
def function(x: int = 0, y: Literal[0] = 0) -> int:
return x
x: Final = 0
y: Literal[0] = 0
function(y, y)
function(x, x)
如您所见,当声明某个值时Final
,我们会创建一个常量。该常量不可更改。它与Literal
实际值一致。这两种类型的源代码实现也非常相似。
为什么我总是在python
simple 中调用依赖类型?因为它目前仅限于简单值:int
、str
、bool
、float
、None
。它目前无法处理元组、列表、字典、自定义类型和类等。不过,你可以在此线程中跟踪开发进度。
不要忘记官方文档。
@超载
接下来我们需要的是@overload
装饰器。它需要定义多个具有不同输入类型和结果的函数声明。
想象一下,我们需要编写一个函数来减少一个值。它应该同时处理str
和int
输入。当输入 时,str
它应该返回除最后一个字符之外的所有输入字符;当输入 时,int
它应该返回前一个数字。
from typing import Union
def decrease(first: Union[str, int]) -> Union[str, int]:
if isinstance(first, int):
return first - 1
return first[:-1]
reveal_type(decrease(1))
# => Revealed type is 'Union[builtins.str, builtins.int]'
reveal_type(decrease('abc'))
# => Revealed type is 'Union[builtins.str, builtins.int]'
不太实用,不是吗?mypy
仍然不知道返回的具体类型。我们可以使用@overload
装饰器来增强类型。
from typing import Union, overload
@overload
def decrease(first: str) -> str:
"""Decreases a string."""
@overload
def decrease(first: int) -> int:
"""Decreases a number."""
def decrease(first: Union[str, int]) -> Union[str, int]:
if isinstance(first, int):
return first - 1
return first[:-1]
reveal_type(decrease(1))
# => Revealed type is 'builtins.int'
reveal_type(decrease('abc'))
# => Revealed type is 'builtins.str'
在本例中,我们定义了几个函数头,以提供mypy
足够的信息来说明正在发生的事情。这些函数头仅在模块进行类型检查时使用。正如您所见,只有一个函数定义实际上包含了一些逻辑。您可以根据需要创建任意数量的函数头。
其思路是:每当mypy
找到一个有多个头的函数时@overload
,它都会尝试将输入值与这些声明进行匹配。当找到第一个匹配项时,它会返回结果类型。
官方文档也可能帮助您了解如何在项目中使用它。
依赖类型
现在,我们将结合关于Literal
和的新知识@overload
来解决我们的问题open
。终于!
记住,我们需要返回bytes
模式'rb'
和str
模式'r'
。
并且我们需要知道确切的返回类型。
算法将是:
- 编写几个
@overload
装饰器来匹配所有可能的情况 - 当我们期望
Literal[]
获得或'r'
'rb'
- 在一般情况下编写函数逻辑
from typing import IO, Any, Union, overload
from typing_extensions import Literal
@overload
def open_file(filename: str, mode: Literal['r']) -> IO[str]:
"""When 'r' is supplied we return 'str'."""
@overload
def open_file(filename: str, mode: Literal['rb']) -> IO[bytes]:
"""When 'rb' is supplied we return 'bytes' instead of a 'str'."""
@overload
def open_file(filename: str, mode: str) -> IO[Any]:
"""Any other options might return Any-thing!."""
def open_file(filename: str, mode: str) -> IO[Any]:
return open(filename, mode)
reveal_type(open_file('some.txt', 'r'))
# => Revealed type is 'typing.IO[builtins.str]'
reveal_type(open_file('some.txt', 'rb'))
# => Revealed type is 'typing.IO[builtins.bytes]'
reveal_type(open_file('some.txt', 'other'))
# => Revealed type is 'typing.IO[AnyStr]'
这里我们得到了什么?三个@overload
装饰器和一个带逻辑的函数体。第一个装饰器声明了参数的@overload
返回值,第二个装饰器声明了使用参数时返回值。第三个装饰器是 fallback 函数。每当我们传入任何其他模式时,我们都可以同时使用和。str
'r'
Literal
bytes
'rb'
str
bytes
现在,我们的问题解决了。我们向函数传入一些特定的值,并接收一些特定的类型作为返回值。这使得我们的代码更易于阅读,执行起来也更安全。
感谢依赖类型如何工作python
!
结论
希望这篇小教程能帮助你python
更好地理解输入法。在以后的文章中,我会探讨更复杂的主题mypy
。
在 Github 上关注我,订阅我的博客和新的开源项目动态。