Как аннотировать тип OrderedDict, который инициализируется литералами?

Предположим, у меня есть следующее:

from collections import OrderedDict
from dataclasses import dataclass


@dataclass
class HelloWorld:
    x: OrderedDict[str, int]


a = OrderedDict([("a", 0), ("c", 2), ("b", 1)])
HelloWorld(a) <--- # type error here

Возникает ошибка типа:

Argument of type "OrderedDict[Literal['a', 'c', 'b'], Literal[0, 2, 1]]" cannot be assigned to parameter "x" of type "OrderedDict[str, int]" in function "__init__"
  "OrderedDict[Literal['a', 'c', 'b'], Literal[0, 2, 1]]" is incompatible with "OrderedDict[str, int]"
    Type parameter "_KT@OrderedDict" is invariant, but "Literal['a', 'c', 'b']" is not the same as "str"
    Type parameter "_VT@OrderedDict" is invariant, but "Literal[0, 2, 1]" is not the same as "int

Как ни странно, этот очень похожий фрагмент не выдает ошибки:

from collections import OrderedDict
from dataclasses import dataclass


@dataclass
class HelloWorld:
    x: OrderedDict[str, int]


HelloWorld(OrderedDict([("a", 0), ("c", 2), ("b", 1)])) # <--- no error
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Используйте typing.cast:

from collections import OrderedDict
from dataclasses import dataclass
from typing import cast


@dataclass
class HelloWorld:
    x: OrderedDict[str, int]


a = OrderedDict([("a", 0), ("c", 2), ("b", 1)])
HelloWorld(cast(OrderedDict[str, int], a))
Ответ принят как подходящий

cast часто является приемлемым решением, но здесь вы можете просто аннотировать исходную переменную. Это сохранит типобезопасность вашего кода (в отличие от cast, который одинаково хорошо может приводить str к dict[int, int]). Пайрайту не следовало жаловаться на следующее:

from collections import OrderedDict
from dataclasses import dataclass

@dataclass
class HelloWorld:
    x: OrderedDict[str, int]

a: OrderedDict[str, int] = OrderedDict([("a", 0), ("c", 2), ("b", 1)])
HelloWorld(x=a)

А вот и детская площадка.

Эта ошибка вызвана слишком оптимистичным выводом pyright вместе с инвариантностью OrderedDict (как и любой MutableMapping).

Как заметил @InSync в комментариях, в Python 3.7 и выше вы можете построить OrderedDict из литерала dict, все еще сохраняя порядок , и в этом случае вывод менее конкретен:

a = OrderedDict({"a": 0, "c": 2, "b": 1})
HelloWorld(x=a)

В этом конкретном случае, возможно, лучше написать OrderedDict({"a": 0, "c": 2, "b": 1}), который, как предполагается, относится к типу OrderedDict[str, int] без явной аннотации. Примечание о перегрузке, которую выбрал Пайрайт, также будет хорошим дополнением к этому ответу.

InSync 27.07.2024 10:07

@InSync, спасибо! Я включил буквальное предложение dict, но едва понимаю, какое отношение оно имеет к выбору перегрузки. Это больше похоже на детали вывода внутреннего пирайта — буквальный dict внутри вызова expr выводится как dict[str, int], а его пары кортежей выводятся как кортежи литералов. Не могли бы вы объяснить, почему, по вашему мнению, здесь важно конкретное overload решение?

STerliakov 27.07.2024 16:55

Предполагается, что элементы кортежа относятся к типам Literal (например, предполагается, что ("a", 0) относится к типу tuple[Literal["a"], Literal[0]]). Таким образом, список соответствует аргументу dict(iterable: Iterable[tuple[_KT, _VT]]), при этом типы элементов объединены (Literal["a", "b", "c"] как _KT и Literal[0, 1, 2] как _VT; вы можете навести курсор на вызов OrderedDict, чтобы подтвердить это). Возможно, это намеренный выбор, потому что в противном случае Пайрайт счел бы [("a", 0), ("c", 2), ("b", 1)] типом list[tuple[str, int]].

InSync 27.07.2024 17:22

Ах, ну, пирайт просто не выводит буквальные литералы dict, ни в вызовах, ни в автономном режиме (reveal_type({'a': 1})), и вот почему ваше предложение работает. Я думаю, достаточно ясно, что вывод в исходном фрагменте основан на типах элементов кортежа, но не стесняйтесь редактировать эту часть в моем ответе или публиковать как свою собственную :)

STerliakov 27.07.2024 17:27

@InSync, но я не могу контролировать порядок вставки при использовании dict в качестве структуры ввода, верно?

bzm3r 28.07.2024 23:55

@bzm3r dict заказан с версии 3.7 (3.6, если вы используете CPython), как отмечено в ответе. На самом деле, OrderedDict сейчас сохранился просто по историческим причинам.

InSync 28.07.2024 23:56

@InSync О да, я только что нашел это: stackoverflow.com/questions/39980323/…

bzm3r 28.07.2024 23:56

Таким образом, я мог бы также использовать dict, а не OrderedDict, и проблемы с заказом, которые я видел, вероятно, были результатом того, что я использовал set для хранения интересующих ключей? Но, может быть, set сейчас тоже заказывают, как следствие того, что dict заказали? Не уверен. Это вызывает много вопросов, но хорошего характера.

bzm3r 28.07.2024 23:57

@bzm3r set вообще не заказан, в этом легко убедиться (например, попробуйте {'a'*n for n in range(1,6)}). set в Python не реализован как словарь с пустыми значениями.

STerliakov 29.07.2024 00:08

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