Подсказки типа Python для общих *args (в частности, zip или zipWith)

Я пишу функцию с именем zip_with со следующей подписью:

_A = TypeVar("_A")
_B = TypeVar("_B")
_C = TypeVar("_C")


def zip_with(zipper: Callable[[_A, _B], _C], a_vals: Iterable[_A], b_vals: Iterable[_B]) -> Generator[_C, None, None]: ...

Это похоже на zip, но позволяет выполнять агрегацию с любой произвольной функцией. Это прекрасно работает для реализации zip_with, которая допускает только 2 аргумента.

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

def zip_with(zipper: Callable[..., _C], *vals: Iterable) -> Generator[_C, None, None]: ...

Другими словами, я хочу, чтобы средство проверки типов могло сопоставлять типы *vals с входными аргументами zipper.


person hoogamaphone    schedule 12.06.2019    source источник


Ответы (1)


К сожалению, не существует чистого способа выражения такой сигнатуры типа. Для этого нам нужна функция под названием вариативные дженерики. Несмотря на то, что есть общий интерес к добавлению этой концепции в PEP 484, в ближайшее время этого, вероятно, не произойдет.

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


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

from typing import TypeVar, overload, Callable, Iterable, Any, Generator

_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T3 = TypeVar("_T3")
_T4 = TypeVar("_T4")
_T5 = TypeVar("_T5")

_TRet = TypeVar("_TRet")

@overload
def zip_with(zipper: Callable[[_T1, _T2], _TRet], 
             __vals1: Iterable[_T1],
             __vals2: Iterable[_T2],
             ) -> Generator[_TRet, None, None]: ...
@overload
def zip_with(zipper: Callable[[_T1, _T2, _T3], _TRet], 
             __vals1: Iterable[_T1],
             __vals2: Iterable[_T2],
             __vals3: Iterable[_T3],
             ) -> Generator[_TRet, None, None]: ...
@overload
def zip_with(zipper: Callable[[_T1, _T2, _T3, _T4], _TRet], 
             __vals1: Iterable[_T1],
             __vals2: Iterable[_T2],
             __vals3: Iterable[_T3],
             __vals4: Iterable[_T4],
             ) -> Generator[_TRet, None, None]: ...
@overload
def zip_with(zipper: Callable[[_T1, _T2, _T3, _T4, _T5], _TRet], 
             __vals1: Iterable[_T1],
             __vals2: Iterable[_T2],
             __vals3: Iterable[_T3],
             __vals4: Iterable[_T4],
             __vals5: Iterable[_T5],
             ) -> Generator[_TRet, None, None]: ...

# One final fallback overload if we want to handle callables with more than
# 5 args more gracefully. (We can omit this if we want to bias towards
# full precision at the cost of usability.)
@overload
def zip_with(zipper: Callable[..., _TRet],
             *__vals: Iterable[Any],
             ) -> Generator[_TRet, None, None]: ...

def zip_with(zipper: Callable[..., _TRet],
             *__vals: Iterable[Any],
             ) -> Generator[_TRet, None, None]:
    pass

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

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

И на самом деле именно этот метод используется для определения типов для zip: https://github.com/python/typeshed/blob/master/stdlib/2and3/builtins.pyi#L1403

person Michael0x2a    schedule 12.06.2019