Сопоставление шаблонов по вложенным типам `Union` в Python

Создавая библиотеку Python, я использую подсказки типов, чтобы гарантировать согласованность определенного представления данных. В частности, я использую Union (типы суммы) во вложенном виде, чтобы представить различные варианты данных, которые могут принимать данные.

То, что я получаю до сих пор, похоже на следующий пример:

from typing import Union

MyNumberT = Union[float,int]
MyDataT = Union[str,MyNumber]

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, float):
        return _my_number_to_string(datum)
    elif isinstance(datum, int):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

def _my_number_to_string(number: MyNumberT) -> str:
    return "%s" % number

Какой тип проверяется нормально, используя mypy.

Теперь мой реальный код немного сложнее, и мне нужно выполнить некоторые общие операции с переменными типа MyNumberT. В примере это просто подчеркивается путем адаптации import и замены my_data_to_string, как показано ниже:

from typing import get_args, Union

[...]

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, get_args(MyNumberT)):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

[...]

На котором проверка типа mypy не удалась: Argument 1 to "_my_number_to_string" has incompatible type "Union[str, Union[float, int]]"; expected "Union[float, int]" .

Я ожидал, что mypy поймет, что в первой ветке datum может быть только типа float или int, но сообщение об ошибке указывает, что это не так...

Как я могу добиться сопоставления с образцом для частей таких вложенных типов?


person Pamplemousse    schedule 27.01.2021    source источник
comment
Просто для ясности: вы хотите сравнить datum с MyNumberT, не повторяя его составляющие?   -  person MisterMiyagi    schedule 27.01.2021
comment
Да! Я хочу не раскрывать детали субсоюза MyNumberT в этой функции.   -  person Pamplemousse    schedule 27.01.2021


Ответы (2)


Ваш вариант использования — отличный пример использования утилиты, предоставленной functools, под названием singledispatch. Это позволяет вам определить несколько функций для одной функции в зависимости от типа ввода.

from functools import singledispatch

# This class defines the function with
# a base case if the input type doesn't match
@singledispatch
def my_data_to_string(datum) -> str:
    raise TypeError(f"unsupported format: {type(datum)}")

# Registering for type str using type hint
@my_data_to_string.register
def _(datum: str):
    return datum

# Registering for multiple 
# types using decorator
@my_data_to_string.register(float)
@my_data_to_string.register(int)
def _(datum):
    return "<%s>" % datum


print(my_data_to_string("a"))    # a
print(my_data_to_string(1))      # <1>
print(my_data_to_string(1.5))    # <1.5>
print(my_data_to_string([1, 2])) # TypeError

Он расширяемый, читабельный и не вызывает ошибок в линтерах/форматтерах. ссылка на документы.

person AnkurSaxena    schedule 27.01.2021
comment
Хотя это, безусловно, правильный подход к основной проблеме, на самом деле он, похоже, не отвечает на вопрос. - person MisterMiyagi; 27.01.2021
comment
Интересный подход на самом деле. Однако я вижу две проблемы: 1) В моем коде больше типов в Union и больше функций для работы с ним (следовательно, больше совпадений с шаблонами), этот синтаксис подразумевает множество декораторов, а также множество дополнительных объявлений функций; 2) Что еще более важно, он просит автора функции знать обо всех внутренних деталях Union, чтобы обрабатывать каждый конечный тип в декораторе, что как бы противоречит идее работы с типами данных высокого уровня. - person Pamplemousse; 27.01.2021

Начиная с Python 3.10, объединения допустимы для проверок isinstance:

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, MyNumberT):
        return _my_number_to_string(datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity

Пока достаточно исключить один компонент объединения, обратная проверка работает без требований:

def my_data_to_string(datum: MyDataT) -> str:
    if isinstance(datum, str):  # handle explicit type first
        return datum
    else:  # catch-all for remaining types
        return _my_number_to_string(datum)
    # rely on type checker for safety!

Обратите внимание, что здесь используется предложение else вместо предложения elif — полагайтесь на средство проверки типов, чтобы отклонить неправильно введенные аргументы.


Для более сложных типов можно построить защиту типа:

def guard_mnt(arg: MyDataT) -> Union[Literal[False], Tuple[MyNumberT]]:
    return (arg,) if isinstance(arg, get_args(MyNumberT)) else False  # type: ignore

Это сообщает программе проверки типов, что она либо вернет желаемый тип в оболочке, либо что-то ложное. type: ignore требуется, так как он использует ту же реализацию проверки типа; функция служит для добавления допустимой проверки статического типа вокруг неподдерживаемой проверки времени выполнения.

Его можно использовать с помощью выражений присваивания и распаковки:

def my_data_to_string(datum: MyDataT) -> str:
    if nums := guard_mnt(datum):  # only enter branch if guard is not False
        return _my_number_to_string(*datum)
    elif isinstance(datum, str):
        return datum
    # assert_never omitted for simplicity
person MisterMiyagi    schedule 27.01.2021
comment
Гарда типа выглядит интересной идеей. Хотя есть две вещи, которые я не понимаю: 1) Почему должен присутствовать # type: ignore? 2) Почему arg помещается внутрь кортежа... Можно ли вернуть его напрямую (как MyNumberT)? Кроме того, механизм assert_never кажется сломанным ("assert_never" has incompatible type "Union[float,int]"; expected "NoReturn" - что соответствует MyNumberT...). - person Pamplemousse; 27.01.2021
comment
#type: ignore необходим, потому что охранник по-прежнему использует тот же механизм, что и в вопросе (isinstance + get_args), который на самом деле не понимает средство проверки типов, и поэтому предполагает, что (arg,) является Tuple[MydataT]. Кортеж необходим, чтобы гарантировать, что результат истинен, если тип соответствует; оба 0 и 0.0 ложны, но (0,) и (0.0,) верны. - person MisterMiyagi; 28.01.2021