Как заставить Mypy понять, что значение по умолчанию не будет использоваться в определенных случаях - PullRequest
1 голос
/ 17 июня 2019

У меня есть следующая функция:

#!/usr/bin/env python3
from typing import Union

def foo(b: Union[str, int]) -> int:
    def bar(a: int = b) -> int: # Incompatible default for argument "a" (default has type "Union[str, int]", argument has type "int")
        return a + 1

    if isinstance(b, str):
        return bar(0)
    else:
        return bar()

print(foo(3)) # 4
print(foo("hello")) # 1

В строке, где я определяю bar, Mypy говорит, что настройка b по умолчанию не будет работать.

Однако из-за того, как работает программа, единственный способ использовать значение по умолчанию b - это если b является целым числом. Так что это должно работать нормально.

Но Мипи этого не понимает.

Как я могу

  1. Получите Mypy, чтобы понять, что int является правильным типом для a или
  2. Исправьте это некоторым способом, который не вызывает слишком много дублирования кода.

(Например, я знаю, что мог бы написать две foo функции с разными сигнатурами, но это было бы слишком много дублирования кода.)

TL; DR ниже - это только мой реальный пример использования, поскольку по крайней мере один ответ основывался на том, насколько простым был мой MCVE выше.

Это функция, которая принимает словарь. Функция возвращает декоратор, который при использовании добавит декорированную функцию (декорированная функция - TypeChecker) к словарю. Декоратор допускает параметр, который указывает имя / ключ, под которым декорированная функция (TypeChecker) находится в словаре. Если имя не указано, оно будет использовать другую функцию (StringHelper.str_function) для определения имени по свойствам самой функции.

Из-за того, как работают параметры декоратора, создатель декоратора должен принимать либо имя (или ничего), либо функцию. Если для этого требуется только функция, то имя не указано, и оно должно получить имя из функции. Если оно принимает только имя, оно будет вызвано снова в функции, и имя должно быть использовано. Если для этого ничего не требуется, то он снова будет вызван в функции и должен получить имя из функции.

def get_type_checker_decorator(type_checkers: Dict[str, TypeChecker]) -> Callable[[Union[Optional[str], TypeChecker]], Union[Callable[[TypeChecker], TypeChecker], TypeChecker]]:
    @overload
    def type_checker_decorator(name: Optional[str]) -> Callable[[TypeChecker], TypeChecker]:
        pass
    @overload
    def type_checker_decorator(name: TypeChecker) -> TypeChecker:
        pass
    def type_checker_decorator(name: Union[Optional[str], TypeChecker] = None) -> Union[Callable[[TypeChecker], TypeChecker], TypeChecker]:
        # if name is a function, then the default will never be used
        def inner_decorator(function: TypeChecker, name: Optional[str] = name) -> TypeChecker: # this gives the Mypy error
            if name is None:
                name = StringHelper.str_function(function)
            type_checkers[name] = function
            def wrapper(string: str) -> bool:
                return function(string)
            return wrapper

        if callable(name):
            # they just gave us the function right away without a name
            function = name
            name = None
            return inner_decorator(function, name)
        else:
            assert isinstance(name, str) or name is None
            # should be called with just the function as a parameter
            # the name will default to the given name (which may be None)
            return inner_decorator

    return type_checker_decorator

Ответы [ 3 ]

1 голос
/ 17 июня 2019

Неловко вызывать сигнатуру типа, если это не совсем то, что ожидает функция. Ваша bar функция явно ожидает int, и форсирование Union в подсказке типа только для того, чтобы позже утверждать, что вы на самом деле принимаете только int s, не нужно для того, чтобы заставить замолчать mypy.

Поскольку вы принимаете b в качестве значения по умолчанию для бара, вам следует позаботиться о случае str внутри bar, поскольку сигнатура типа b уже указана в foo. Два альтернативных решения, которые я бы посчитал более подходящими для рассматриваемой проблемы:

def foo(b: Union[str, int]) -> int:

    # bar takes care of the str case. Type of b already documented
    def bar(a=b) -> int:
        if isinstance(b, str):
            return bar(0)
        return a + 1

    return bar()

Определение значения по умолчанию перед определением bar:

def foo(b: Union[str, int]) -> int:

    x: int = 0 if isinstance(b, str) else b

    # bar does not take a default type it won't use.
    def bar(a: int = x) -> int:
        return a + 1

    return bar()
0 голосов
/ 17 июня 2019

Это значение по умолчанию даже не должно существовать.Безопасный способ написания этого кода был бы

def foo(b: Union[str, int]) -> int:
    def bar(a) -> int:
        return a + 1

    if isinstance(b, str):
        return bar(0)
    else:
        return bar(b)

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


Ваш код очень похож на попытку сделать

def f(x: Union[int, str]) -> int:
    y: int = x
    if isinstance(x, str):
        return 1
    else:
        return y + 1

При таком написании кажется очевидным, что y неверно.Нам не следует присваивать что-либо переменной статического типа int, если мы на самом деле не знаем в точке назначения, что это int.Было бы неразумно ожидать, что средство проверки типов будет проверять все пути кода, которые могут привести к использованию y, чтобы определить, что это безопасно.Проверка типа значения аргумента по умолчанию следует аналогичному принципу;это проверяется на основе информации, доступной, когда установлено значение по умолчанию, а не на путях кода, где его можно использовать.

Для еще более экстремального примера рассмотрим

def f(x: Union[int, str]) -> int:
    def g(y: int = x):
        pass
    return 4

y никогда не будет использоваться .Средство проверки типов по-прежнему сообщает о несоответствии типов, и об этом будут сообщения об ошибках, если средство проверки типов не сообщит об этом.

0 голосов
/ 17 июня 2019

При написании этого вопроса, я думаю, что я ответил на него сам, так что я мог бы также поделиться.

Решение немного неловко, но это лучшее, что я мог придумать, и, кажется, работает.Сначала введите a как целое число или строку.Затем, в качестве первой строки функции, подтвердите, что a - это int.

Вместе они выглядят как

#!/usr/bin/env python3
from typing import Union

def foo(b: Union[str, int]) -> int:
    def bar(a: Union[int, str] = b) -> int: # Incompatible default for argument "a" (default has type "Union[str, int]", argument has type "int")
        assert isinstance(a, int)
        return a + 1

    if isinstance(b, str):
        return bar(0)
    else:
        return bar()

print(foo(3)) # 4
print(foo("hello")) # 1

Это решение не идеально, хотя,так как если вы будете следовать сигнатуре функции и передать ей строку, произойдет сбой (из-за assert).Таким образом, он требует некоторого самоанализа самой функции, чтобы определить, что на самом деле может быть передано.

...