Python 中的简单依赖类型

2025-06-10

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我们可以得到strint的值。结果类型将是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

文字

Literaltype 表示特定类型的特定值。

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 = 0Literal类型。因为 的值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实际值一致。这两种类型的源代码实现也非常相似。

为什么我总是在pythonsimple 中调用依赖类型?因为它目前仅限于简单值:intstrboolfloatNone。它目前无法处理元组、列表、字典、自定义类型和类等。不过,你可以在此线程中跟踪开发进度。

不要忘记官方文档

@超载

接下来我们需要的是@overload装饰器。它需要定义多个具有不同输入类型和结果的函数声明。

想象一下,我们需要编写一个函数来减少一个值。它应该同时处理strint输入。当输入 时,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'
并且我们需要知道确切的返回类型。

算法将是:

  1. 编写几个@overload装饰器来匹配所有可能的情况
  2. 当我们期望Literal[]获得'r''rb'
  3. 在一般情况下编写函数逻辑
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' Literalbytes'rb'strbytes

现在,我们的问题解决了。我们向函数传入一些特定的值,并接收一些特定的类型作为返回值。这使得我们的代码更易于阅读,执行起来也更安全。

感谢依赖类型如何工作python

结论

希望这篇小教程能帮助你python更好地理解输入法。在以后的文章中,我会探讨更复杂的主题mypy
在 Github 上关注我,订阅我的博客和新的开源项目动态。

鏂囩珷鏉ユ簮锛�https://dev.to/wemake-services/simple-dependent-types-in-python-4e14
PREV
写作让我成为一名更好的工程师
NEXT
Python 异常被视为反模式