跳转到主要内容

运行时实现@typing.overload。

项目描述

超越

如何安装?

pip install overtake

超越相当自包含。默认情况下,唯一的依赖项是typing-extensions

什么是超越?

你有没有想过多次声明同一个函数?

def count_words(arg: str) -> int:
    return len(arg.split())


def count_words(arg: list[str]) -> int:
    return sum(len(text.split()) for text in arg)


print(count_words("one two three!"))
# 3
print(count_words(["one two", "three four five six"]))
# 6

那么你就在正确的位置!

超越是一个小型库,旨在将@typing.overload推得更远。@typing.overload仅定义签名,以便类型检查器知道在调用函数时有哪些类型提示可用。使用超越,您还可以在运行时调用这些函数,类似于C++或Julia等语言。

举例说明胜过千言万语

from typing import overload

# if you are using Python <3.11, you'll need instead
# from typing_extensions import overload

from overtake import overtake


@overload
def count_words(arg: str) -> int:
    return len(arg.split())


@overload
def count_words(arg: list[str]) -> int:
    return sum(len(text.split()) for text in arg)


@overtake(runtime_type_checker="beartype")
def count_words(arg):
    ...


print(count_words("one two three!"))
# 3
print(count_words(["one two", "three four five six"]))
# 6

超越将分析类型和提供的参数以调用正确的实现。

它支持@typing.overload支持的每个签名@typing.overload

此模式由IDE(Pycharm、VSCode等)支持,因此自动补全将工作得很好。它也受到类型检查器(Mypy、Pyright等)的支持,因此您无需在类型安全方面做出妥协 😁

超越紧密遵循Mypy关于@typing.overload的指南:https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading

更多高级示例。

我们可以在这里向您展示更多可能的模式。基本上,if isinstance(..., ...)可能是您使用overtake来编写更清晰代码的提示。

递归

让我们编写一个返回自1月1日起天数数量的函数

from typing import overload
from datetime import date

from overtake import overtake


@overload
def days_this_year(current_date: date) -> int:
    delta = current_date - date(2023, 1, 1)
    return delta.days


@overload
def days_this_year(current_date: str) -> int:
    return days_this_year(date.fromisoformat(current_date))


@overtake
def days_this_year(current_date):
    ...


print(days_this_year(date(2023, 8, 15)))
# 226
print(days_this_year("2023-08-15"))
# 226

您可以使用递归方式调用您的函数,以消除代码重复。我们实际上可以像这样重写我们的第一个例子

from typing import overload

from overtake import overtake


@overload
def count_words(input_value: str) -> int:
    return len(input_value.split())


@overload
def count_words(input_value: list[str]) -> int:
    return sum(count_words(text) for text in input_value)


@overtake(runtime_type_checker="beartype")
def count_words(input_value):
    ...


print(count_words("one two three!"))
# 3
print(count_words(["one two", "three four five six"]))
# 6

不同的输出类型

也可以有不同的输出类型,比如使用@overload

from typing import overload

from overtake import overtake


@overload
def convert_to_int(input_value: str) -> int:
    return int(input_value)


@overload
def convert_to_int(input_value: list[str]) -> list[int]:
    return [int(x) for x in input_value]


@overtake(runtime_type_checker="beartype")
def convert_to_int(input_value):
    ...


print(convert_to_int("88"))
# 88 (an integer)
print(convert_to_int(["88", "42", "84"]))
# [88, 42, 84] (a list of integers)

利用可选参数

它可以避免一些令人讨厌的if ... is None:使用,您可以指定不同数量的参数(但顺序必须匹配!)。

对于这个示例,假设您想让用户能够在任何类型的文件中写入一些文本。如果没有提供文件,我们将创建一个临时文件。我们必须接受对文件的任何输入。可以是 strpathlib.Path,也可以是文件对象。或者什么都不提供(随机文件)。

from typing import overload
from pathlib import Path
import io
import random

from overtake import overtake


@overload
def write_text_to_file(text: str, file: io.TextIOBase) -> None:
    file.write(text)


@overload
def write_text_to_file(text: str, file: Path) -> Path:
    file.write_text(text)
    return file


@overload
def write_text_to_file(text: str, file: str) -> Path:
    return write_text_to_file(text, Path(file))


@overload
def write_text_to_file(text: str) -> Path:
    random_file_name = f"/tmp/{random.randint(0, 10)}.txt"
    return write_text_to_file(text, random_file_name)


@overtake(runtime_type_checker="beartype")
def write_text_to_file(text, file=None):
    ...


print(write_text_to_file("hello world"))
# /tmp/4.txt
print(write_text_to_file("hello world", "/tmp/some-file.txt"))
# /tmp/some-file.txt
print(write_text_to_file("hello world", Path("/tmp/some-file.txt")))
# /tmp/some-file.txt
print(write_text_to_file("hello world", io.StringIO()))
# None (we didn't write in a file on disk)

利用关键字参数

您可以使用这种范式来强制用户使用比函数中定义的更少的参数。这里有一个例子,您正在寻找用户的余额。您可以提供用户ID或用户名,但不能两者都提供。

from overtake import overtake
from typing import overload


@overload
def find_user_balance(name: str) -> int:
    return 40


@overload
def find_user_balance(user_id: int) -> int:
    return 50


@overtake
def find_user_balance(*, user_id=None, name=None):
    ...


print(find_user_balance(name="Julie"))
# 40
print(find_user_balance(user_id=14))
# 50

在这种情况下,Overtake可以为您节省很多时间。您不需要检查user_idname是否都提供,也不需要检查它们中是否没有任何一个提供。Overtake会为您做这些!

建议

我们建议使用您选择的类型检查器(Mypy、Pyright等),以便类型检查器可以捕获对@overload的无效使用。尽管这不是强制的,但早期捕获@overload的错误是很有帮助的。

runtime_type_checker 参数

Python中的运行时类型检查很困难。实际上,非常困难。一些库提供了这项功能,如beartypepydantic

为了避免安装任何依赖项,overtake可以使用isinstance作为默认的"basic"类型检查器。请注意,这仅适用于类类型。例如,它不适用于list[str],但适用于listdatetime

要处理更复杂的数据类型,您应该使用

@overtake(runtime_type_checker="beartype")
def find_user_balance(*, user_id=None, name=None):
    ...

@overtake(runtime_type_checker="pydantic")
def find_user_balance(*, user_id=None, name=None):
    ...

我需要什么样的 runtime_type_checker?有这么多选择!!!

首先,不要选择任何(默认使用"basic"),看看它是否工作。如果您使用的类型太复杂(如果您正在使用泛型或协议),Overtake将引发错误,并告诉您使用另一个runtime_type_checker

然后,您可以选择使用"beartype""pydantic"。问问自己,“我是否需要beartype特定的类型?比如beartype的验证器?” 如果是,请使用beartype。

如果不是,问问自己“我是否需要Pydantic特定的类型?比如Pydantic的urls或自定义类型?” 如果是,请使用pydantic。

如果您在做出决定后仍然犹豫,您应该使用beartype,因为它更快(验证时间复杂度为O(1)),但请注意,您可能会遇到不支持类型的静默错误,这可能会非常不愉快。请参阅这个问题。Pydantic可能较慢,但在使用不支持类型时您会收到错误。因此,它更安全。

这两个库都非常酷,我鼓励任何好奇的心去阅读那些文档!我们建议您使用pip install overtake[beartype]pip install overtake[pydantic]安装这些库。

我可以用Beartype类型检查器做些什么有趣的事情?

我一直在等你问。看看!

from overtake import overtake
from typing import overload, Annotated
from beartype.vale import Is

# Type hint matching only strings with lengths ranging [4, 40].
LengthyString = Annotated[str, Is[lambda text: 4 <= len(text) < 40]]

# Type hint matching only strings with lengths ranging [0, 4].
ShortString = Annotated[str, Is[lambda text: 0 <= len(text) < 4]]


@overload
def is_this_string_big(arg: ShortString) -> str:
    return "This is a short string!"


@overload
def is_this_string_big(arg: LengthyString) -> str:
    return "This is a very long string"


@overtake(runtime_type_checker="beartype")
def is_this_string_big(arg):
    ...


print(is_this_string_big("Hi!"))
# This is a short string!
print(is_this_string_big("No one expects the spanish inquisition!"))
# This is a very long string

您还需要if语句吗?人们可能会想。

我可以用Pydantic类型检查器做些什么有趣的事情?

您可以玩他们的自定义类型!

from typing import overload

from overtake import overtake
from pydantic import MongoDsn, RedisDsn


@overload
def connect(arg: RedisDsn) -> str:
    return "Connected to redis!"


@overload
def connect(arg: MongoDsn) -> str:
    return "Connected to MongoDB!"


@overtake(runtime_type_checker="pydantic")
def connect(arg):
    ...


print(connect("rediss://:pass@localhost"))
# Connected to redis!
print(connect("mongodb://mongodb0.example.com:27017"))
# Connected to MongoDB!

与Pyright的兼容性

Pyright有一个小的兼容性问题,您可能会遇到以下错误

error: "my_function" is marked as overload, but it includes an implementation
The body of a function overload should be "..."

这可以通过在重载函数签名旁边添加# type: ignore来解决,而无需禁用调用和函数体的类型检查。这里有一个小示例

from datetime import date

from overtake import overtake
from typing_extensions import overload


@overload
def days_this_year(current_date: date) -> int:  # type: ignore
    delta = current_date - date(2023, 1, 1)
    return delta.days


@overload
def days_this_year(current_date: str) -> int:  # type: ignore
    return days_this_year(date.fromisoformat(current_date))


@overtake
def days_this_year(current_date):
    ...

为了长期解决这个限制(Mypy有一天可能会引发相同的错误),已经制定了一个PEP草案。不要犹豫,在讨论中提出反馈!

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源代码分发

overtake-0.4.0.tar.gz (15.7 kB 查看哈希值)

上传时间 源代码

构建分发

overtake-0.4.0-py3-none-any.whl (13.5 kB 查看哈希值)

上传时间 Python 3

支持者