Можете ли вы указать дисперсию в аннотации типа Python? - PullRequest
1 голос
/ 14 марта 2019

Можете ли вы обнаружить ошибку в коде ниже? Mypy не может.

from typing import Dict, Any

def add_items(d: Dict[str, Any]) -> None:
    d['foo'] = 5

d: Dict[str, str] = {}
add_items(d)

for key, value in d.items():
    print(f"{repr(key)}: {repr(value.lower())}")

Python замечает ошибку, конечно, услужливо сообщая нам, что 'int' object has no attribute 'lower'. Жаль, что это не может сказать нам об этом до времени выполнения.

Насколько я могу судить, mypy не улавливает эту ошибку, потому что позволяет аргументам параметра d add_items быть ковариантными. Это имело бы смысл, если бы мы только читали из словаря. Если бы мы только читали, то мы бы хотели, чтобы параметр был ковариантным. Если мы готовы читать любой тип, мы должны иметь возможность читать строковые типы. Конечно, если мы только читаем, то мы должны напечатать это как typing.Mapping.

Поскольку мы пишем, мы действительно хотим, чтобы параметр был контравариантным . Например, для кого-то вполне логично было бы передать Dict[Any, Any], поскольку он вполне мог бы хранить строковый ключ и целочисленное значение.

Если бы мы читали записи и , не было бы выбора, кроме как для параметра, который должен быть инвариантным.

Есть ли способ указать, какой тип дисперсии нам нужен? Более того, достаточно ли mypy достаточно сложен, чтобы можно было ожидать, что он определит дисперсию посредством статического анализа, и это следует рассматривать как ошибку? Или текущее состояние проверки типов в Python просто не в состоянии отловить такую ​​программную ошибку?

1 Ответ

1 голос
/ 14 марта 2019

Ваш анализ неверен - на самом деле это не имеет ничего общего с дисперсией, и тип Dict в mypy фактически не зависит от его значения.

Скорее проблема в том, что вы объявили значениевашего Dict, чтобы иметь тип Any, динамический тип.Это фактически означает, что вы хотите, чтобы mypy просто не проверял тип ничего, связанного со значениями вашего Dict.И поскольку вы отказались от проверки типов, она, естественно, не обнаружит никаких ошибок, связанных с типами.

(Это достигается волшебным размещением Any как вверху, так и внизу типа.решетка. В основном, для некоторого типа T, это тот случай, когда Any всегда является подтипом T , а T всегда является подтипом Any. Mypy автоматически выбирает, какое отношение приводит кошибок нет.)

Вы можете увидеть, что Dict инвариантен для вас самих, запустив следующую программу:

from typing import Dict

class A: pass
class B(A): pass
class C(B): pass

def accepts_a(x: Dict[str, A]) -> None: pass
def accepts_b(x: Dict[str, B]) -> None: pass
def accepts_c(x: Dict[str, C]) -> None: pass

my_dict: Dict[str, B] = {"foo": B()}

# error: Argument 1 to "accepts_a" has incompatible type "Dict[str, B]"; expected "Dict[str, A]"
# note: "Dict" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance
# note: Consider using "Mapping" instead, which is covariant in the value type
accepts_a(my_dict)

# Type checks! No error.
accepts_b(my_dict)

# error: Argument 1 to "accepts_c" has incompatible type "Dict[str, B]"; expected "Dict[str, C]"
accepts_c(my_dict)

Успешен только вызов accept_b, что соответствует ожидаемой дисперсии.


Чтобы ответить на ваш вопрос о том, как установить дисперсию, mypy разработан таким образом, что дисперсия структур данных устанавливается во время определения и не может быть реально изменена во время вызова.

Таким образом, поскольку Dict был определен как инвариантный, вы не можете по-настоящему изменить его как ковариантный или инвариантный.

Подробнее о настройке дисперсии в defвремя начала, см. справочные документы mypy на дженерики .

Как вы указали, вы можете заявить, что хотите принять версию Dict только для чтенияс помощью картографии.Как правило, это тот случай, когда есть версия только для чтения любой структуры данных PEP 484, которую вы, возможно, захотите использовать - например, Sequence - версия List только для чтения.

AFAIK, по умолчанию нет версии только для записиDict хотя.Но вы можете взломать их вместе, используя протоколы , стандартизированный, скорее всего, стандартизированный метод выполнения структурного, а не номинального ввода:

from typing import Dict, TypeVar, Generic
from typing_extensions import Protocol

K = TypeVar('K', contravariant=True)
V = TypeVar('V', contravariant=True)

# Mypy requires the key to also be contravariant. I suspect this is because
# it cannot actually verify all types that satisfy the WriteOnlyDict
# protocol will use the key in an invariant way.
class WriteOnlyDict(Protocol, Generic[K, V]):
    def __setitem__(self, key: K, value: V) -> None: ...

class A: pass
class B(A): pass
class C(B): pass

# All three functions accept only objects that implement the
# __setitem__ method with the signature described in the protocol.
#
# You can also use only this method inside of the function bodies,
# enforcing the write-only nature.
def accepts_a(x: WriteOnlyDict[str, A]) -> None: pass
def accepts_b(x: WriteOnlyDict[str, B]) -> None: pass
def accepts_c(x: WriteOnlyDict[str, C]) -> None: pass

my_dict: WriteOnlyDict[str, B] = {"foo": B()}

#  error: Argument 1 to "accepts_a" has incompatible type "WriteOnlyDict[str, B]"; expected "WriteOnlyDict[str, A]"
accepts_a(my_dict)

# Both type-checks
accepts_b(my_dict)
accepts_c(my_dict)

Чтобы ответить на ваш неявный вопрос («Как заставить mypy обнаружить ошибку типа здесь / как правильно проверить тип моего кода?»), Ответ «простой» - просто избегайте использования Any любой ценой.Каждый раз, когда вы это делаете, вы намеренно открываете дыру в системе типов.

Например, более безопасный для типов способ объявить, что значения вашего dict могут быть чем угодно, было бы использовать Dict[str, object].И теперь mypy пометил бы вызов функции add_items как не типизированный.

Или, в качестве альтернативы, рассмотрите возможность использования TypedDict , если вы знаете, что ваши значения будут неоднородными.

Вы даже можете заставить mypy запретить использование некоторых Any, включив Отключить динамический набор семейство флагов командной строки / флагов файла конфигурации.

Тем не менее, на практикеполностью запретить использование Any часто нереально.Даже если вы можете встретить этот идеал в своем коде, многие сторонние библиотеки либо аннотированы, либо не полностью аннотированы, что означает, что они прибегают к использованию Any повсеместно.Так что, вообще, к сожалению, отказ от их использования, как правило, требует много дополнительной работы.

...