Проверка типов с условными параметрами

Я пытаюсь использовать ввод с функцией с условными параметрами, которая работает следующим образом:

from typing import Optional, Union


class Foo:
    some_param_to_check: str = 'foo_name'
    one_param_exclusive_to_foo: int


class Bar:
    some_param_to_check: str = 'bar_name'
    another_param_exclusive_to_bar: str


def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar],
    a_name: str,
) -> bool:
    return f_or_b.some_param_to_check == a_name


def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    if not foo and not bar:
        raise ValueError('You need to specify either "foo" or "bar".')

    # I added this explicit type hint after the first error, hoping it would solve the issue:
    foo_or_bar: Union[Foo, Bar]  # later becomes Union[Foo, Bar, None]
    foo_or_bar = foo if foo else bar

    return some_process_that_returns_a_bool(foo_or_bar, some_name)


foo_obj = Foo()
bar_obj = Bar()

# This will work:
do_something_with_foo_or_bar(foo_obj, bar_obj, 'test_string')

# This will also work:
do_something_with_foo_or_bar(foo_obj, None, 'test_string')

# This too:
do_something_with_foo_or_bar(None, bar_obj, 'test_string')

# But this should not:
do_something_with_foo_or_bar(None, None, 'test_string')

Чтобы добавить больше контекста:

Функция работает, ожидая foo или, если она недоступна, bar. Если foo не является None, bar будет игнорироваться.

При проверке с помощью mypy он жалуется на:

Incompatible types in assignment (expression has type "Union[Foo, Bar, None]", variable has type "Union[Foo, Bar]"

(Я предполагаю, что из-за Optional в подсказках типа параметра.)

Если я затем добавлю None в качестве подсказки типа для foo_or_bar, то ошибка станет такой:

error: Item "None" of "Union[Foo, Bar, None]" has no attribute "some_param_to_check"

Как мне исправить это, чтобы mypy перестал жаловаться (при этом сохраняя подсказки типа)?

Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
131
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы получаете сообщение об ошибке, потому что в some_process_that_returns_a_bool вы пытаетесь получить доступ some_param_to_check к значению, которое может быть None, а None не имеет этого свойства.

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

def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar, None],
    a_name: str,
) -> bool:
    if f_or_b is None:
      # Handle None...
      return False
    else:
      return f_or_b.some_param_to_check == a_name

Таким образом, вы попытаетесь получить доступ к some_param_to_check только тогда, когда f_or_b не является None, а в противном случае вы вернетесь False, когда f_or_b будет None.

Но, пожалуйста, обращайтесь с None так, как это имеет смысл для вашего приложения.

Union[Optional[Foo], Optional[Bar] запутанный способ написания Union[Foo, Bar, None]
juanpa.arrivillaga 28.11.2022 06:22

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

juanpa.arrivillaga 28.11.2022 06:25

@juanpa.arrivillaga Ты прав. Предполагаемый момент в этом примере заключался только в том, что должна быть реализована проверка None, если None является предполагаемым возможным значением для аргумента. Я отредактирую текст, чтобы отразить это.

Azer 28.11.2022 06:32
Ответ принят как подходящий

Я почти уверен, что это просто проблема mypy. Его система вывода типов недостаточно умна, чтобы распознать, что ваш блок if not foo and not bar, который вызывает исключение, впоследствии исключает двойной случай None (поскольку он не может сделать окончательный вывод ни о каком типе по отдельности). Кажется, нет хорошего способа напрямую исправить подсказку типа, но вместо этого вы могли бы немного изменить логику, чтобы более четко разделить случаи, и mypy должен понять это лучше:

def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    foo_or_bar: Union[Foo, Bar]
    if foo:
        foo_or_bar = foo
    elif bar:
        foo_or_bar = bar
    else:
        raise ValueError('You need to specify either "foo" or "bar".')

    return some_process_that_returns_a_bool(foo_or_bar, some_name)

Вы также можете полностью избавиться от переменной foo_or_bar и просто поместить два разных вызова функций в блоки if и elif, используя foo или bar в зависимости от ситуации.

Добавление моего $ 0,02 к ответу @Blckknght:

Это решает проблему с внутренней проверкой типов (нет ошибок внутри тела функции, если вы исправите отсутствующую аннотацию), но не помогает внешним вызывающим объектам.

Чтобы добиться ввода «того или иного, но никогда ни одного из них», вы можете использовать перегрузки:

from typing import Optional, Union, overload


class Foo:
    some_param_to_check: str = 'foo_name'
    one_param_exclusive_to_foo: int


class Bar:
    some_param_to_check: str = 'bar_name'
    another_param_exclusive_to_bar: str


def some_process_that_returns_a_bool(
    f_or_b: Union[Foo, Bar],
    a_name: str,
) -> bool:
    return f_or_b.some_param_to_check == a_name

@overload
def do_something_with_foo_or_bar(
    foo: Foo,
    bar: Optional[Bar],
    some_name: str,
) -> bool: ...
@overload
def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Bar,
    some_name: str,
) -> bool: ...
def do_something_with_foo_or_bar(
    foo: Optional[Foo],
    bar: Optional[Bar],
    some_name: str,
) -> bool:
    foo_or_bar: Union[Foo, Bar]  # This annotation was missing
    if foo:
        foo_or_bar = foo
    elif bar:
        foo_or_bar = bar
    else:
        raise ValueError('You need to specify either "foo" or "bar".')

    return some_process_that_returns_a_bool(foo_or_bar, some_name)
    

do_something_with_foo_or_bar(Foo(), Bar(), '')
do_something_with_foo_or_bar(None, Bar(), '')
do_something_with_foo_or_bar(Foo(), None, '')
do_something_with_foo_or_bar(None, None, '')  # Line 51

И вуаля:

main.py:51: error: No overload variant of "do_something_with_foo_or_bar" matches argument types "None", "None", "str"  [call-overload]
main.py:51: note: Possible overload variants:
main.py:51: note:     def do_something_with_foo_or_bar(foo: Foo, bar: Optional[Bar], some_name: str) -> bool
main.py:51: note:     def do_something_with_foo_or_bar(foo: Optional[Foo], bar: Bar, some_name: str) -> bool

Вот ссылка на игровую площадку.

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

Похожие вопросы