Есть ли предпочтительный способ создания псевдонимов типов для составных типов с помощью модуля ввода Python?

У меня есть функция с одним параметром, которая должна принимать int или None в качестве аргумента. Есть несколько способов создать псевдоним типа для такого составного типа:

# test.py
import typing

IntOrNone_1 = typing.TypeVar('IntOrNone_1', int, None)
IntOrNone_2 = typing.Union[int, None]


def my_func1(xyz: IntOrNone_1):
    return xyz

def my_func2(xyz: IntOrNone_2):
    return xyz


my_func1(12)
my_func1(None)
my_func1(13.7)
my_func1('str')

my_func2(12)
my_func2(None)
my_func2(13.7)
my_func2('str')

Оба метода делают то, что я от них ожидаю, однако соответствующие ошибки немного отличаются, но в основном имеют одинаковое значение.

test.py:14: ошибка: значение переменной типа «IntOrNone_1» из «my_func1» не может быть «плавающим»

test.py:15: ошибка: значение переменной типа «IntOrNone_1» из «my_func1» не может быть «str»

test.py:19: ошибка: аргумент 1 для «my_func2» имеет несовместимый тип «float»; ожидается "Необязательный [int]"

test.py:20: ошибка: аргумент 1 для «my_func2» имеет несовместимый тип «str»; ожидается "Необязательный [int]"

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

Действительно ли оба метода эквивалентны, как я полагаю, или один из них предпочтительнее?


person MaxPowers    schedule 25.03.2019    source источник


Ответы (1)


Эти два метода далеко не эквивалентны. Вам не следует думать о TypeVars как о простых псевдонимах - скорее, это более специальные формы, которые вы используете, когда хотите сделать свои функции универсальными.

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

Один из способов, который мы могли бы сделать, - это попробовать использовать object:

def identity(x: object) -> object:
    return x

Это приближает нас, поскольку наша функция identity может по крайней мере принимать буквально все (поскольку все типы наследуются от object в Python). Однако это решение ошибочно: если мы передаем int, мы возвращаемся object, а это не то, что мы хотим.

Скорее, нам нужен способ, чтобы средство проверки типов могло понять, что между этими двумя типами существует «связь». Вот тут-то и пригодится TypeVar:

T = TypeVar('T')

def identity(x: T) -> T:
    return x

Наша TypeVar 'T' в этом случае действует как «заполнитель», который может быть привязан к любому типу, который мы хотим. Так что, если мы сделаем identity(3), T будет привязан к int - таким образом, средство проверки типов поймет, что тип возвращаемого значения также должен быть int!

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


Итак, что делает приведенное ниже выражение?

IntOrNone_1 = typing.TypeVar('IntOrNone_1', int, None)

Что ж, оказывается, иногда может быть полезно добавить ограничения к нашему специальному типу заполнителя. Например, вы ограничили IntOrNone_1, чтобы его можно было привязать только к int или None, и никаким другим типам.


И наконец, чтобы ответить на ваш последний вопрос: в приведенных вами примерах вы обязательно должны использовать Union, а не TypeVars.

Независимо от того, используете ли вы Union или псевдоним типа для Union, это действительно вопрос личного вкуса, но если вам не нужен этот «заполнитель» или «общее» поведение, вам не следует использовать TypeVars.

В документации mypy есть раздел по универсальным шаблонам, если вы хотите узнать больше о как использовать TypeVars. В нем рассказывается о нескольких вещах, которые я пропустил, в том числе о том, как создавать универсальные классы (а не только универсальные функции).

person Michael0x2a    schedule 25.03.2019