Аннотации типов для * args и ** kwargs

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

Например, как выразить, что разумные аргументы функции - это либо int, либо два int? 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?


person Praxeolitic    schedule 04.05.2016    source источник


Ответы (6)


Для переменных позиционных аргументов (*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 или более и не может быть ограничен подсказками типа более конкретным диапазоном.

person Martijn Pieters    schedule 04.05.2016
comment
Просто любопытно, зачем добавлять Optional? Что-то изменилось в Python или вы передумали? Это все еще не является строго необходимым из-за None значения по умолчанию? - person Praxeolitic; 25.08.2017
comment
@Praxeolitic: да, на практике автоматическая подразумеваемая аннотация Optional, когда вы используете None в качестве значения по умолчанию, усложнила определенные варианты использования, и теперь она удаляется из PEP. - person Martijn Pieters; 25.08.2017
comment
Вот ссылка для обсуждения этого для заинтересованных. Это определенно звучит так, будто в будущем потребуется явное Optional. - person Rick supports Monica; 06.02.2018
comment
На самом деле это не поддерживается для Callable: github.com/python/mypy/issues/5876 - person Shital Shah; 11.01.2020
comment
@ShitalShah: проблема не в этом. Callable не поддерживает любое упоминание подсказки типа для *args или **kwargs точки. Эта конкретная проблема связана с разметкой вызываемых объектов, которые принимают определенные аргументы плюс произвольное количество других, и поэтому используйте *args: Any, **kwargs: Any, очень конкретную подсказку типа для двух универсальных аргументов. В случаях, когда вы устанавливаете *args и / или **kwargs на что-то более конкретное, вы можете использовать Protocol. - person Martijn Pieters; 11.01.2020

Правильный способ сделать это - использовать @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 вне файлов-заглушек < / а>.

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

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))
person chadrik    schedule 24.08.2017
comment
Мне нравится этот ответ, потому что он касается более общего случая. Оглядываясь назад, мне не следовало использовать в качестве примера вызовы функций (type1) vs (type1, type1). Возможно, (type1) vs (type2, type1) были бы лучшим примером и показывали бы, почему мне нравится этот ответ. Это также позволяет использовать разные типы возвращаемых данных. Однако в особом случае, когда у вас есть только один возвращаемый тип, а ваши *args и *kwargs относятся к одному типу, метод в ответе Марджина имеет больше смысла, поэтому полезны оба ответа. - person Praxeolitic; 25.08.2017
comment
Однако использование *args при максимальном количестве аргументов (здесь 2) все еще неверно. - person Martijn Pieters; 25.08.2017
comment
Итак, да, это хорошо знать о @overload, но это неподходящий инструмент для этой конкретной работы. - person Martijn Pieters; 25.08.2017
comment
@MartijnPieters Почему здесь *args обязательно неправильно? Если ожидаемые вызовы были (type1) против (type2, type1), то количество аргументов является переменным и нет подходящего значения по умолчанию для конечного аргумента. Почему так важно, чтобы был максимум? - person Praxeolitic; 25.08.2017
comment
*args действительно существует для нуля или более, неограниченных, однородных аргументов, или для «передачи этих нетронутых» универсальных аргументов. У вас есть один обязательный аргумент и один необязательный. Это совершенно другое дело и обычно обрабатывается путем передачи второму аргументу контрольного значения по умолчанию для обнаружения того, что оно было опущено. - person Martijn Pieters; 25.08.2017
comment
Использование *args, где количество элементов равно не менее n, не более m, где n больше 0, нарушает самодокументированный характер и соглашение, в то время как аргументы ключевого слова для необязательных параметров имеют очень давний прецедент в Python. - person Martijn Pieters; 25.08.2017
comment
Посмотрев на PEP, становится ясно, что @overload не используется по назначению. Хотя этот ответ показывает интересный способ индивидуального аннотирования типов *args, еще лучший ответ на вопрос заключается в том, что это вообще не то, что следует делать. - person Praxeolitic; 25.08.2017
comment
@MartijnPieters Я убежден в *args, но как насчет **kwargs? Похоже, что аннотирование отдельных типов для **kwargs могло бы соответствовать стилю Python, но просто нет способа сделать это. Вы бы согласились? - person Praxeolitic; 25.08.2017
comment
Это все еще единственный ответ, полностью отвечающий на исходный вопрос. Есть ли способ аннотировать возможные типы * args и ** kwargs, делая акцент на возможных. Я не уверен, почему меня не голосуют за то, что является очевидным правильным ответом. Пример был сфабрикован, но цель была ясна: OP хотел явно описать возможные сигнатуры функции foo. - person chadrik; 26.08.2017
comment
По крайней мере, в MyPy рекомендуется использовать перегрузки, а не прямой вызов. Таким образом, пользователю не рекомендуется передавать меньше или больше позиционных аргументов, чем необходимо. Кажется, это стандарт, так как он работает и в PyCharm. Это решение позволяет нескольким разнородным перегрузкам совместно использовать общую базовую реализацию, но с проверкой не во время выполнения, и поэтому я считаю лучшим решением. Это может быть решение для реализации чего-то, что считается непитоническим, но это второстепенная проблема. - person TerrestrialIntelligence; 27.12.2019
comment
@MartijnPieters, вы писали: у вас есть один обязательный аргумент и один необязательный. Это совершенно другое дело и обычно обрабатывается путем передачи второму аргументу контрольного значения по умолчанию для обнаружения того, что оно было опущено. Я согласен с тем, что аргумент ключевого слова был бы более ясным, но даже в этом случае вам все равно нужно будет использовать @overload для правильного ввода этой функции, если тип результата или первый аргумент менялись в зависимости от существования второго аргумента. например если допустимые подписи типа были (int) -> int и (int, int) -> float - person chadrik; 06.01.2020
comment
@chadrik - это реальный вариант использования @overload, который ортогонален обсуждаемой здесь проблеме. Другими словами, не имеет значения, что количество аргументов меняется, вам также понадобится @overload для (int) -> int vs (float) -> float, или (bytes, int) -> bytes vs (str, int) -> str и т. Д. - person Martijn Pieters; 07.01.2020

В качестве краткого дополнения к предыдущему ответу, если вы пытаетесь использовать mypy в файлах Python 2 и вам нужно использовать комментарии для добавления типов вместо аннотаций, вам необходимо префикс типов для args и kwargs с помощью * и ** соответственно:

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

Mypy рассматривает это как то же самое, что и нижеследующая версия foo для Python 3.5:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
person Michael0x2a    schedule 28.06.2016

Пока не поддерживается

Хотя вы можете аннотировать вариативные аргументы типом, я не считаю это очень полезным, поскольку предполагает, что все аргументы имеют один и тот же тип.

Соответствующая аннотация типа *args и **kwargs, которая позволяет указывать каждый вариативный аргумент отдельно, пока не поддерживается mypy. Есть предложение добавить Expand помощника в модуль mypy_extensions, он будет работать так:

class Options(TypedDict):
    timeout: int
    alternative: str
    on_error: Callable[[int], None]
    on_timeout: Callable[[], None]
    ...

def fun(x: int, *, **options: Expand[Options]) -> None:
    ...

Проблема GitHub была открыта в январе 2018 г., но до сих пор не закрыта. Обратите внимание, что, хотя проблема касается **kwargs, синтаксис Expand, скорее всего, также будет использоваться для *args.

person Cesar Canassa    schedule 23.08.2020

В некоторых случаях содержимое ** kwargs может быть разных типов.

Мне кажется, это работает:

from typing import Any

def testfunc(**kwargs: Any) -> None:
    print(kwargs)

or

from typing import Any, Optional

def testfunc(**kwargs: Optional[Any]) -> None:
    print(kwargs)

В случае, когда вы чувствуете необходимость ограничить типы в **kwargs, я предлагаю создать объект, подобный структуре, и добавить туда типизацию. Это можно сделать с помощью классов данных или pydantic.

from dataclasses import dataclass

@dataclass
class MyTypedKwargs:
   expected_variable: str
   other_expected_variable: int


def testfunc(expectedargs: MyTypedKwargs) -> None:
    pass
person monkut    schedule 26.03.2021
comment
Это по сути отключает проверку типов, не так ли? Это все равно, что вообще исключить аннотацию для kwargs. - person normanius; 07.04.2021
comment
**kwargs по дизайну и технически может быть что угодно. Если вы знаете, что получаете, я предлагаю определить это как типизированный аргумент. Преимущество здесь в том, что в случаях, когда использование **kwargs допустимо / ожидается, в ides / tools, таких как pycharm, оно не даст вам уведомления о неправильном типе. - person monkut; 07.04.2021
comment
Частично не согласен. Я думаю, что есть ситуации, когда разумно ограничивать типы для ** kwargs или * args. Но я также вижу, что проверка типов и ** kwargs не очень хорошо сочетаются друг с другом (по крайней мере, для текущих версий Python). Возможно, вы хотите добавить это к своему ответу, чтобы лучше адресовать вопрос OP. - person normanius; 10.04.2021
comment
Да, может быть вариант использования для набора kwargs, но я бы предпочел сделать ваши вводимые данные более понятными, а не объединять их в kwargs. - person monkut; 12.04.2021

Если кто-то хочет описать конкретные именованные аргументы, ожидаемые в kwargs, вместо этого можно передать TypedDict (который определяет обязательные и необязательные параметры). Необязательные параметры - это какие были kwargs. Примечание. TypedDict находится на python ›= 3.8 См. Этот пример. :

import typing

class RequiredProps(typing.TypedDict):
    # all of these must be present
    a: int
    b: str

class OptionalProps(typing.TypedDict, total=False):
    # these can be included or they can be omitted
    c: int
    d: int

class ReqAndOptional(RequiredProps, OptionalProps):
    pass

def hi(req_and_optional: ReqAndOptional):
    print(req_and_optional)
person spacether    schedule 04.03.2021
comment
Спасибо; хорошая точка зрения. Только что добавил эту информацию - person spacether; 26.07.2021