В предыдущих версиях нашего приложения люди просто передавали некоторые аргументы в виде простых строк определенным функциям, поскольку для некоторых из них у нас не было конкретных подсказок типов или типов данных. Что-то вроде:
# 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
, независимо от того, существует ли этот модуль во время выполнения. Итак, TypeAlias
и синтаксис объединения int | str
доступны в Python 3.9 следующим образом, если вам не нужно анализировать аннотации во время выполнения:
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!
Что произойдет, если вы используете
Mapping
вместоDict
?