Как набирать словари подсказок, которые могут иметь разные пользовательские ключи и/или значения?

В предыдущих версиях нашего приложения люди просто передавали некоторые аргументы в виде простых строк определенным функциям, поскольку для некоторых из них у нас не было конкретных подсказок типов или типов данных. Что-то вроде:

# 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, которые могут принимать пользовательские типы ключей (как в примере) и даже пользовательские типы значений или комбинацию вышеперечисленного.

Что произойдет, если вы используете Mapping вместо Dict?

mkrieger1 21.06.2024 20:52
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
1
138
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная проблема заключается в следующем:

Конечно, делаю что-то вроде:

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 варианта:

  1. Обеспечьте более строгую типизацию, попросив пользователей комментировать (из вопроса неясно, кто предоставляет определения DataType - вы/сопровождающие библиотеки или пользователи)?

  2. Не просите пользователей комментировать, а установите @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!

Другие вопросы по теме