Введіть анотації для * args та ** kwargs


158

Я намагаюсь примітки типу Python із абстрактними базовими класами для написання деяких інтерфейсів. Чи є спосіб анотувати можливі типи *argsта **kwargs?

Наприклад, як можна виразити, що розумні аргументи функції - це intабо два, або два ints? type(args)дає, Tupleтак що моє здогадка було анотувати тип як Union[Tuple[int, int], Tuple[int]], але це не працює.

from typing import Union, Tuple

def foo(*args: Union[Tuple[int, int], Tuple[int]]):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

# ok
print(foo((1,)))
print(foo((1, 2)))
# mypy does not like this
print(foo(1))
print(foo(1, 2))

Повідомлення про помилки з mypy:

t.py: note: In function "foo":
t.py:6: error: Unsupported operand types for + ("tuple" and "Union[Tuple[int, int], Tuple[int]]")
t.py: note: At top level:
t.py:12: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:14: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 2 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"

Має сенс, що mypy не подобається це для виклику функції, оскільки він очікує, що tupleв самому дзвінку буде місце. Додавання після розпакування також дає помилку введення тексту, яку я не розумію.

Як коментувати чутливі типи для *argsта **kwargs?

Відповіді:


167

Для змінних позиційних аргументів ( *args) та аргументів змінних ключових слів ( **kw) вам потрібно вказати лише очікуване значення для одного такого аргументу.

З Довільних списків аргументів і значення по замовчуванням аргументу секції з типу підказок PEP:

Списки довільних аргументів можуть бути також анотовані, так що визначення:

def foo(*args: str, **kwds: int): ...

є прийнятним, і це означає, що, наприклад, всі наступні представляють виклики функцій з допустимими типами аргументів:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

Отже, ви хочете вказати свій метод так:

def foo(*args: int):

Однак якщо ваша функція може приймати лише одне або два цілих значення, вам взагалі не слід користуватися *args, використовуйте один явний позиційний аргумент та другий аргумент ключового слова:

def foo(first: int, second: Optional[int] = None):

Тепер ваша функція фактично обмежена одним або двома аргументами, і обидва повинні бути цілими числами, якщо вони вказані. *args завжди означає 0 або більше, і не може бути обмежений типовими підказками до більш конкретного діапазону.


1
Просто цікаво, навіщо додавати Optional? Щось змінилося в Python чи ви передумали? Це все-таки не є строго необхідним через Noneдефолт?
Праксеоліт

10
@Praxeolitic так, на практиці автоматична підразумеваемая Optionalанотація, коли ви використовуєте Noneяк значення за замовчуванням, ускладнювала певні шаблони використання, які тепер видаляються з PEP.
Martijn Pieters

5
Ось посилання, що обговорює це для зацікавлених. Це, безумовно, здається, що явне Optionalбуде потрібно в майбутньому.
Рік підтримує Моніку

Це насправді не підтримується для Callable
Shital Shah

1
@ShitalShah: це не зовсім те, про що йдеться. Callableне підтримує будь - яких згадки про типу натяк на *argsабо **kwargs повної зупинки . Ця специфічна проблема стосується розмітки дзвінків, які приймають конкретні аргументи плюс довільну кількість інших , і, таким чином, використовують *args: Any, **kwargs: Any, дуже специфічний натяк на тип для двох ловушок. У випадках, коли ви встановлюєте *argsта / або **kwargsщось більш конкретне, ви можете використовувати Protocol.
Martijn Pieters

26

Правильний спосіб зробити це - використання @overload

from typing import overload

@overload
def foo(arg1: int, arg2: int) -> int:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

Зауважте, що ви не додаєте @overloadта не вводите анотації до фактичної реалізації, яка має бути останньою.

Вам знадобиться typingнова версія обох і mypy, щоб отримати підтримку @overload поза файлами stub .

Ви також можете використовувати це для зміни повернутого результату таким чином, щоб чітко вказати, які типи аргументів відповідають якому типу повернення. наприклад:

from typing import Tuple, overload

@overload
def foo(arg1: int, arg2: int) -> Tuple[int, int]:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return j, i
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

2
Мені подобається ця відповідь, оскільки вона стосується більш загального випадку. Озираючись назад, я не повинен був використовувати дзвінки функції (type1)vs (type1, type1)як свій приклад. Можливо, (type1)vs (type2, type1)був би кращим прикладом і показує, чому мені подобається ця відповідь. Це також дозволяє різні типи повернення. Однак у спеціальному випадку, коли у вас є лише один тип повернення і ваш, *argsі *kwargsвсі вони одного типу, техніка у відповіді Мартиніна має більше сенсу, тому обидві відповіді корисні.
праксеоліт

4
Однак використання *argsтам, де є максимальна кількість аргументів (2 тут), все ще помиляється .
Мартійн Пітерс

1
@MartijnPieters Чому тут *argsобов'язково помиляється? Якщо очікувані виклики були (type1)vs (type2, type1), то кількість аргументів є змінною, і для аргументу, що відкладається, не існує відповідного за замовчуванням. Чому важливо мати максимум?
Праксеоліт

1
*argsнасправді існує для нуля або більше , неоднорідних, однорідних аргументів, або для того, щоб "передавати їх уздовж недоторканих" ловів. У вас є один необхідний аргумент і один необов’язковий. Це зовсім інше і, як правило, обробляється, надаючи другому аргументу значення довідкового за замовчуванням, щоб виявити, що це було опущено.
Martijn Pieters

3
Переглянувши PEP, це явно не є цільовим використанням @overload. Хоча ця відповідь показує цікавий спосіб індивідуальної анотації типів *args, ще краща відповідь на питання полягає в тому, що це зовсім не те, що слід робити взагалі.
Праксеоліт

20

Як коротке доповнення до попередньої відповіді, якщо ви намагаєтесь використовувати mypy на файлах Python 2 і вам потрібно використовувати коментарі для додавання типів замість анотацій, вам слід встановити типи для args та kwargsз *і **відповідно:

def foo(param, *args, **kwargs):
    # type: (bool, *str, **int) -> None
    pass

Mypy трактує це як те саме, що нижче, версія Python 3.5 foo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.