Ошибка mypy, перегрузка с помощью Union / Optional, «Перегруженные сигнатуры функций 1 и 2 пересекаются с несовместимыми типами возврата» - PullRequest
0 голосов
/ 29 ноября 2018

Итак, начнем с примера.Предположим, у нас есть несколько типов, которые можно объединить, скажем, мы используем __add__ для реализации этого.К сожалению, из-за не зависящих от нас обстоятельств все должно быть «обнуляемым», поэтому мы вынуждены использовать Optional везде.

from typing import Optional, List, overload
class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value
    def __add__(self, other: 'Foo') -> 'Optional[Foo]':
        result = self.value - other.value
        if result > 42:
            return None
        else:
            return Foo(result)

class Bar:
    value: str
    def __init__(self, value: str) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Bar]':
        if len(self.value) + len(other.value) > 42:
            return None
        else:
            return Bar(self.value + other.value)

class Baz:
    value: List[str]
    def __init__(self, value:List[str]) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Baz]':
        if len(self.value) + 1 > 42:
            return None
        else:
            return Baz([*self.value, other.value])


@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

Мы хотим, чтобы вспомогательная функция выполняла для нас нулевую проверку, но в целом могла обрабатывать «комбинируемые» типы.Большинство типов можно комбинировать только с самими собой, и, чтобы быть более верным моему реальному варианту использования, скажем, один тип сочетается с другим.Я бы надеялся, что декоратор overload мог бы помочь здесь, однако mypy жалуется:

mcve4.py:35: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
mcve4.py:35: error: Overloaded function signatures 1 and 3 overlap with incompatible return types
mcve4.py:38: error: Overloaded function signatures 2 and 3 overlap with incompatible return types

Используя версию mypy: mypy 0.641

Обратите внимание, если я удалю Optional madnessMypy не жалуется.Я даже могу оставить один из них необязательным!:

from typing import List, overload
class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value
    def __add__(self, other: 'Foo') -> 'Foo':
        result = self.value - other.value
        return Foo(result)

class Bar:
    value: str
    def __init__(self, value: str) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Bar':
        return Bar(self.value + other.value)

class Baz:
    value: List[str]
    def __init__(self, value:List[str]) -> None:
        self.value = value
    def __add__(self, other: 'Bar') -> 'Optional[Baz]':
        return Baz([*self.value, other.value])


@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> 'Optional[Baz]':
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

Это заставляет меня подозревать, что «перекрытие» относится к NoneType, но мне кажется, что это должно быть решаемо, я полностью сбился с базы?

Редактировать

Так что, я действительно просто колеблюсь здесь, но я полагаю, когда оба аргумента None, это определенно неоднозначно, я бы надеялся, что следующее разрешит это:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Optional[Foo], that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Bar], that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Baz], that: Optional[Bar]) -> Optional[Baz]:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

Но я все еще получаю:

mcve4.py:37: error: Overloaded function signatures 2 and 3 overlap with incompatible return types
mcve4.py:37: error: Overloaded function signatures 2 and 4 overlap with incompatible return types
mcve4.py:40: error: Overloaded function signatures 3 and 4 overlap with incompatible return types

Edit2 Идя по той же тропе, я попробовал следующее:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Foo, that: Optional[Foo]) -> Optional[Foo]:
    ...
@overload
def Add(this: Optional[Foo], that: Foo) -> Optional[Foo]:
    ...
@overload
def Add(this: Baz, that: Bar) -> Optional[Baz]:
    ...
@overload
def Add(this: Baz, that: Optional[Bar]) -> Optional[Baz]:
    ...
@overload
def Add(this: Optional[Baz], that: Bar) -> Optional[Baz]: # 6
    ...
@overload
def Add(this: Bar, that: Optional[Bar]) -> Optional[Bar]:
    ...
@overload
def Add(this: Optional[Bar], that: Bar) -> Optional[Bar]: # 8
    ...

def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

Я сейчасПолучение:

mcve4.py:49: error: Overloaded function signatures 6 and 8 overlap with incompatible return types

Что начинает иметь смысл для меня, я думаю, что в основном то, что я пытаюсь сделать, небезопасно / сломано.Возможно, мне придется просто порезать гордиев узел ...

1 Ответ

0 голосов
/ 29 ноября 2018

Боюсь, не будет особенно чистого способа решения этой проблемы - по крайней мере, такого, о котором я лично не знаю.Как вы заметили, ваши сигнатуры типов содержат фундаментальную неоднозначность, которую mypy не допустит: если вы попытаетесь вызвать Add с аргументом типа None, mypy принципиально не сможет определить, какой из указанных вариантов перегрузкисовпадения.

Подробнее об этом см. в документации mypy по проверке инвариантов перегрузки - найдите абзац, обсуждающий «по своей сути небезопасно перекрывающиеся варианты», и начните читать оттуда.


Однако в этом конкретном случае мы можем свободно покачиваться, излагая перегрузки для более точного соответствия фактическому поведению во время выполнения.В частности, у нас есть это замечательное свойство: если один из аргументов равен «None», мы должны также вернуть None.Если мы закодируем это, mypy в конечном итоге будет удовлетворен:

@overload
def Add(this: None, that: None) -> None:
    ...
@overload
def Add(this: Foo, that: None) -> None:
    ...
@overload
def Add(this: Bar, that: None) -> None:
    ...
@overload
def Add(this: Baz, that: None) -> None:
    ...
@overload
def Add(this: None, that: Foo) -> None:
    ...
@overload
def Add(this: None, that: Bar) -> None:
    ...
@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Union[Baz, None]'

Тот факт, что это работает, может поначалу показаться удивительным - в конце концов, мы передаем аргумент типа Optional[...] и все же ни одна из перегрузоксодержат этот тип!

То, что делает mypy здесь, неофициально называется "union math" - оно в основном отмечает, что x и y оба являются объединениями типа Union[Baz, None] и Union[Bar, None] соответственно, ипытается выполнить духовный эквивалент вложенного цикла for, чтобы проверить каждую возможную комбинацию этих союзов.Таким образом, в этом случае он проверяет вариант перегрузки, соответствующий (Baz, Bar), (Baz, None), (None, Bar) и (None, None), и возвращает возвращаемые значения типов Baz, None, None и None соответственно.

Окончательный тип возврата - это объединение этих значений: Union[Baz, None, None, None].Это упрощает до Union[Baz, None], который является желаемым типом возврата.


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

Если это так, то вы могли бы объявить «банкротство» в отношении «Нет»'по всей вашей кодовой базе и запустите mypy с отключенным режимом «строгого необязательного» * ​​1036 *.

Короче говоря, если вы запустите mypy с флагом --no-strict-optional, вы дадите команду mypy предположить, чтоNone является действительным членом каждого класса.Это то же самое, что и то, как Java предполагает, что 'null' является допустимым членом каждого типа.(Хорошо, каждый не примитивный тип, но все что угодно).

Это ослабляет безопасность типов вашего кода (иногда резко), но позволит вам упростить ваш код, чтобы он выглядел так:

class Foo:
    value: int
    def __init__(self, value: int) -> None:
        self.value = value

    # Note: with strict-optional disabled, returning 'Foo' vs
    # 'Optional[Foo]' means the same thing
    def __add__(self, other: 'Foo') -> Foo:
        result = self.value - other.value
        if result > 42:
            return None
        else:
            return Foo(result)


@overload
def Add(this: Foo, that: Foo) -> Foo:
    ...
@overload
def Add(this: Bar, that: Bar) -> Bar:
    ...
@overload
def Add(this: Baz, that: Bar) -> Baz:
    ...
def Add(this, that):
    if this is None or that is None:
        return None
    else:
        return this + that

x: Optional[Baz]
y: Optional[Bar]

reveal_type(Add(x, y))  # Revealed type is 'Baz'

Строго говоря, проверки на перегрузку должны сообщать об ошибке «небезопасного перекрытия» по той же причине, по которой они возвращались, когда был включен строгий необязательный параметр.Однако, если бы мы это сделали, перегрузки были бы совершенно непригодны, когда строгий опциональный режим отключен: поэтому mypy намеренно ослабляет проверки здесь и игнорирует этот конкретный случай ошибки.

Основным недостатком этого режима является то, что вытеперь вынужден делать больше проверки во время выполнения.Если вы получите какое-то значение типа Baz, оно может на самом деле быть None - подобно тому, как любая ссылка на объект в Java может фактически быть null.

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

Если вы подпишетесь на школу мысли "ошибка ноль была миллиардной ошибкой" и хотели бы жить в мире строгих опцийодна из техник, которую вы можете использовать, - это постепенное повторение включения строгого опционального в некоторых частях вашей кодовой базы с помощью файла конфигурации mypy .

По сути, вы можете настроить многие (хотя и не все) параметры mypy для каждого модуля через файл конфигурации, что может быть очень удобно, если вы пытаетесь добавить типы в существующую кодовую базу и обнаружите, что переходвсе сразу просто неразрешимо.Начните со слабых глобальных настроек, а затем постепенно делайте их все строже и строже.


Если оба эти параметра кажутся слишком экстремальными (например, вы не хотите добавлять подробную подпись сверху везде, нотакже не хочу отказываться от строгой опциональности), последний вариант, который вы могли бы сделать, это просто silence ошибка, добавив # type: ignore к каждой строке, mypy сообщает об ошибке "unsafely overlapping types" на.

Это тоже поражение, но, возможно, более локализованное.Даже typhed , хранилище подсказок типов для стандартной библиотеки, содержит несколько разбросанных здесь # type: ignore комментариев для некоторых функций, которые просто не поддаются выражению с использованием типов PEP 484.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...