在我们应用程序的早期版本中,人们只会将一些带有纯字符串的参数传递给某些函数,因为我们没有为其中一些函数提供特定的类型提示或数据类型。比如:
# Hidden function signature:
def dummy(var: str):
pass
# Users:
dummy("cat")
但现在我们希望为这些函数签名实现自定义数据类型,同时提供向后兼容性。说这样的话:
# Signature:
def dummy(var: Union[NewDataType, Literal["cat"]])
# Backward compatibility:
dummy("cat")
# New feature:
dummy(NewDataType.cat)
对于简单的函数签名实现这一点很好,但当签名更复杂时就会出现问题。
如果 dummy 的参数是一个可以同时使用
Literal["cat"]
和 NewDataType
作为键的字典,如何实现?此外,如果参数是具有相同先前键类型组合的字典,但也可以将 str
和 int
作为值(以及四种可能的组合),如何实现这一点?所有这些都必须符合 mypy、pylint 并使用 Python 3.9(无 StrEnum
或 TypeAlias
)。
我尝试了许多不同的组合,如下所示:
from typing import TypedDict, Literal, Dict, Union
from enum import Enum
# For old support:
AnimalsLiteral = Literal[
"cat",
"dog",
"snake",
]
# New datatypes:
class Animals(Enum):
cat = "cat"
dog = "dog"
snake = "snake"
# Union of Animals Enum and Literal types for full support:
DataType = Union[Animals, AnimalsLiteral]
# option 1, which fails:
def dummy(a: Dict[DataType, str]):
pass
# option 2, which also fails:
# def dummy(a: Union[Dict[DataType, str], Dict[Animals, str], Dict[AnimalsLiteral, str]]):
# pass
if __name__ == "__main__":
# Dictionary with keys as Animals Enum
input_data1 = {
Animals.dog: "dog",
}
dummy(input_data1)
# Dictionary with keys as Literal["cat", "dog", "snake"]
input_data2 = {
"dog": "dog",
}
dummy(input_data2)
# Dictionary with mixed keys: Animals Enum and Literal string
input_data3 = {
Animals.dog: "dog",
"dog": "dog",
}
dummy(input_data3)
dummy(input_data1)
没问题,但是 dummy(input_data2)
给出了以下 mypy 错误,其中虚拟签名为 2:
Argument 1 to "dummy" has incompatible type "dict[str, str]"; expected "Union[dict[Union[Animals, Literal['cat', 'dog', 'snake']], str], dict[Animals, str], dict[Literal['cat', 'dog', 'snake'], str]]"Mypyarg-type
Argument 1 to "dummy" has incompatible type "dict[str, str]"; expected "Union[dict[Union[Animals, Literal['cat', 'dog', 'snake']], str], dict[Animals, str], dict[Literal['cat', 'dog', 'snake'], str]]"Mypyarg-type
(variable) input_data2: dict[str, str]
当然做类似的事情:
input_data2: DataTypes = {
"dog": "dog",
}
可以解决这个问题,但我不能要求用户在创建数据类型时始终这样做。
另外,我尝试了使用
TypedDict
的另一种替代方法,但我仍然遇到相同类型的 mypy 错误。
最后,我希望能够创建符合 mypy 和 pylint 的字典类型提示,它可以采用自定义键类型(如示例中所示)甚至自定义值类型,或上述的组合。
核心问题是:
当然做类似的事情:
input_data2: DataTypes = { "dog": "dog", }
可以解决这个问题,但我不能要求用户在创建数据类型时始终这样做。
如果您不希望用户提供注释,那么他们必须将数据直接传递给函数 (
dummy({"dog": "dog"})
),以便函数的参数类型推断启动。这是因为当类型检查器推断出来自 dict
的作业中未注释名称的类型,它们不会将类型推断为文字(请参阅 mypy Playground、Pyright Playground):
a = {"dog": "dog"}
reveal_type(a) # dict[str, str]
我怀疑,如果类型检查器试图推断未注释的赋值上的文字键,其他用户会抱怨误报,因为他们想要
dict[str, str]
。 dict[str, str]
永远无法在函数中实现更严格注释的参数 (def dummy(a: Dict[DataType, str]): ...
)。
在我看来,你有2个选择:
通过要求用户注释来实现更严格的输入(从问题中不清楚谁提供
DataType
定义 - 是你/库维护者还是用户)?
不要要求用户进行注释,而是制作一个允许更宽松注释的
@typing.overload
:
from typing import overload
@overload
def dummy(a: dict[DataType, str]): ...
@overload
def dummy(a: dict[str, str]): ...
作为奖励,当 mypy 获得支持时,您可以使用 PEP 702:
@warnings.deprecated
来警告您的用户,如果他们的打字太松散。请参阅Pyright Playground的示例。
附加说明:在您的问题详细信息中,您提到:
所有这些都必须符合 mypy、pylint 并使用 Python 3.9(无
或StrEnum
)。TypeAlias
尚未终止生命的 Python 版本能够利用打字的最新功能。这是因为类型检查器需要理解从
typing_extensions
的导入,无论该模块在运行时是否存在。因此,只要您不需要在运行时内省注释,就可以通过以下方式在 Python 3.9 中使用 TypeAlias
和联合语法 int | str
:
from __future__ import annotations
var1: int | str = 1
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing_extensions import TypeAlias
IntStrAlias: TypeAlias = int | str
# Or
IntStrAlias: TypeAlias = "int | str"
Python 3.11 的
enum.StrEnum
在 Python 3.9 中也很容易被模仿(参见文档中的注释),并且需要类型检查器来理解这一点:
from enum import Enum
class StrEnum(str, Enum):
dog = "dog"
>>> reveal_type(StrEnum.dog) # Literal[StrEnum.dog]
>>> print(StrEnum.dog + " barks!")
dog barks!