Подсказки типов для функции, которая принимает (однородную) последовательность одного из нескольких типов - PullRequest
1 голос
/ 15 февраля 2020

Я пытаюсь предоставить подсказки типа для функции, которая принимает последовательность с одним из двух type элементов, и я не знаю, как сделать mypy счастливым. Пожалуйста, обратите внимание, что последовательность является однородной, то есть типы не могут быть смешаны, или-или. Обычно я делаю это, когда они являются «совместимыми» типами, например, объектом path str или pathlib.Path, и замечание, что с помощью Union работает просто отлично. Но в случае последовательности Sequence[Union[..]] (или Union[Sequence[..]]), похоже, не работает. Вот минимальный рабочий пример:

from pathlib import Path
from typing import Sequence, Dict, Union


def fn_accepts_dict(adict):
    """Function from an external module that accepts `dict`s."""
    for key, val in adict.items():
        print(f"{key}, {val}")


def vararg_test(resources: Sequence[Union[str, Dict]]):
    """My function where I want to provide type hints"""
    if isinstance(resources[0], str):
        resources2 = [{"path": Path(f)} for f in resources]
    else:
        resources2 = resources
    for d in resources2:
        fn_accepts_dict(d)

Теперь, с вышеуказанными определениями, вызов vararg_test с любой из этих двух работ, как и ожидалось:

l1 = ["foo/bar", "bar/baz"]
l2 = [{"path": Path("foo/bar")}, {"path": Path("bar/baz")}]

Но выполнение mypy дает мне следующие ошибки:

type_hints.py:14: error: Argument 1 to "Path" has incompatible type "Union[str, Dict[Any, Any]]"; expected "Union[str, _PathLike[str]]"
type_hints.py:16: error: Incompatible types in assignment (expression has type "Sequence[Union[str, Dict[Any, Any]]]", variable has type "List[Dict[str, Path]]")
Found 2 errors in 1 file (checked 1 source file)

Как я могу решить эту проблему?

Редактировать : Чтобы дать некоторый фон, str это путь, а dict имеет метаданные, соответствующие этому пути, а функция fn_accepts_dict объединяет метаданные в один объект метаданных. Так что логический поток у них либо: str -> dict -> fn_accepts_dict, либо dict -> fn_accepts_dict.

Хотя предложение @ ShadowRanger выглядело многообещающе, не повезло. Я получаю ту же ошибку со следующими подсказками:

def vararg_test2(resources: Union[Sequence[str], Sequence[Dict]]):
    ... # same implementation as above

mypy ошибка:

type_hints.py:24: error: Argument 1 to "Path" has incompatible type "Union[str, Dict[Any, Any]]"; expected "Union[str, _PathLike[str]]"
type_hints.py:26: error: Incompatible types in assignment (expression has type "Union[Sequence[str], Sequence[Dict[Any, Any]]]", variable has type "List[Dict[str, Path]]")

Редактировать 2 : к сожалению, со всеми аннотациями это больше похоже на C / C ++, чем на Python; см. мой собственный ответ ниже для более полного решения Pythoni c.

Ответы [ 3 ]

2 голосов
/ 15 февраля 2020

TLDR: mypy понимает только isinstance(v, tp) относительно v. Он не понимает isinstance(v<expr>, tp) относительно v, например, v[0]: str подразумевает тип v: List[str].


mypy не понимает isinstance(resources[0], str) для перехода между resources: Sequence[str] и resources: Sequence[Dict]. Это означает, что resources = resources2 подразумевает, что оба являются точным того же типа, который либо ломается при присваивании или использовании resources2.

Вы должны аннотировать resources2, чтобы предотвратить равенство выводимых типов, и cast в ветвях, чтобы пометить их как таковые.

def vararg_test(resources: Union[Sequence[str], Sequence[Dict]]):
    """My function where I want to provide type hints"""
    resources2: Sequence[Dict]  # fixed type to prevent inferred equality
    if isinstance(resources[0], str):  # Mypy does not recognise this branch by itself!
        # exclude second Union branch
        resources = cast(Sequence[str], resources)
        resources2 = [{"path": Path(f)} for f in resources]
    else:
        # exclude first Union branch
        resources2 = cast(Sequence[Dict], resources)
    for d in resources2:
        fn_accepts_dict(d)
1 голос
/ 16 февраля 2020

Как указано в ответе @ MisterMiyagi, проблема заключается в том, что mypy не может определить типы, когда выражение используется в isinstance. Поэтому я попробовал альтернативную реализацию, где isinstance передается имя. Это также имело то преимущество, что vararg_test теперь может принимать любую итерацию.

def vararg_test(resources: Iterable[Union[str, Dict]]):
    """My function where I want to use type hints"""
    resources2: List[Dict] = []
    for res in resources:
        if isinstance(res, str):
            res = {"path": Path(res)}
        resources2 += [res]
    for d in resources2:
        fn_accepts_dict(d)

Однако это все же требует, чтобы мы аннотировали resources2, но это легко переформулировать как понимание списка, когда нам не нужно будет ничего аннотации.

def vararg_test3(resources: Iterable[Union[str, Dict]]):
    """My function where I want to use type hints"""
    resources2 = [
        {"path": Path(res)} if isinstance(res, str) else res
        for res in resources
    ]
    for d in resources2:
        fn_accepts_dict(d)
1 голос
/ 15 февраля 2020

Я бы порекомендовал , а не , пытаясь сделать две разные вещи в одной функции в зависимости от типа аргумента. Вместо этого определите две разные функции, каждая из которых принимает определенный c тип последовательности.

def do_with_strings(resources: Sequence[str]):
    do_with_dicts([{"path": Path(f)} for f in resources])


def do_with_dicts(resources: Sequence[dict]):
    for d in resources:
        fn_accepts_dict(d)

Для написанного вами кода тип resources должен быть таким, как ShadowRanger предложил в комментарии, Union[Sequence[str],Sequence[dict]], так как вы предполагаете, что весь список имеет тот же тип, что и первый элемент.

Если вы хотите сохранить гетерогенный тип, вам нужно проверить каждый элемент, чтобы определить, что ему нужно: быть превращенным в dict:

def vararg_test(resources: Sequence[Union[str, Dict]]):
    for f in resources:
        if isinstance(f, str):
            f = {"path": Path(f)}
        fn_accepts_dict(f)
...