Когда я пытаюсь обойтись без *args
и **kwargs
в сигнатурах функций, это не всегда можно сделать, не вредя удобству использования API. Особенно — когда надо писать функции, которые обращаются к вспомогательным функциям с одинаковыми сигнатурами.
Типизация *args
и **kwargs
всегда меня расстраивала, так как их нельзя было заблаговременно снабдить точными аннотациями. Например, если и позиционные, и именованные аргументы функции могут содержать лишь значения одинаковых типов, можно было поступить так:
def foo(*args: int, **kwargs: bool) -> None:
...
Применение такой конструкции указывает на то, что args
— это кортеж, все элементы которого являются целыми числами, а kwargs
— это словарь, ключи которого являются строками, а значения имеют логический тип.
Но нельзя было адекватно аннотировать *args
и **kwargs
в ситуации, когда значения, которые можно передавать в качестве позиционных и именованных аргументов, могут, в разных обстоятельствах, относиться к различным типам. В таких случаях приходилось прибегать к Any
, что противоречило цели типизации аргументов функции.
Взгляните на следующий пример:
def foo(*args: tuple[int, str], **kwargs: dict[str, bool | None]) -> None:
...
Тут система проверки типов воспринимает каждый из позиционных аргументов в виде кортежа из целого числа и строки. Кроме того, она считает каждый именованный аргумент словарём, ключи которого являются строками, а значения — либо сущностями логического типа, либо объектами None
.
При использовании вышеописанных аннотаций mypy не пропустит следующий код:
foo(*(1, "hello"), **{"key1": 1, "key2": False})
error: Argument 1 to "foo" has incompatible type "*tuple[int, str]";
expected "tuple[int, str]" [arg-type]
error: Argument 2 to "foo" has incompatible type "**dict[str, int]";
expected "dict[str, bool | None]" [arg-type]
А вот такое будет признано нормальным:
foo((1, "hello"), kw1={"key1": 1, "key2": False})
Программисту, вероятно, хотелось бы воспользоваться первым вариантом кода, а вот системе проверки типов нужен второй его вариант.
Для того чтобы правильно аннотировать второй пример — нужно прибегнуть к инструментам из PEP-589, PEP-646, PEP-655, и PEP-692. А именно, мы воспользуемся Unpack
и TypedDict
из модуля typing
. Вот как это сделать:
from typing import TypedDict, Unpack # Python 3.12+
# from typing_extensions import TypedDict, Unpack # < Python 3.12
class Kw(TypedDict):
key1: int
key2: bool
def foo(*args: Unpack[tuple[int, str]], **kwargs: Unpack[Kw]) -> None:
...
args = (1, "hello")
kwargs: Kw = {"key1": 1, "key2": False}
foo(*args, **kwargs) # Ok
Тип TypedDict
появился в Python 3.8. Он позволяет аннотировать словари, поддерживающие значения различных типов. Если все значения словаря имеют один и тот же тип — для его аннотирования можно просто воспользоваться конструкцией dict[str, T]
. А вот тип TypedDict
ориентирован на ситуации, когда все ключи словаря являются строками, а значения могут иметь различные типы.
В следующем примере показано то, как можно аннотировать словарь, содержащий значения различных типов:
from typing import TypedDict
class Movie(TypedDict):
name: str
year: int
movies: Movie = {"name": "Mad Max", "year": 2015}
С помощью оператора Unpack
можно показать, что объекты являются распакованными.
Использование TypedDict
с Unpack
позволяет указать системе проверки типов на то, что ей не надо допускать ошибку, считая каждый позиционный и именованный аргументы, соответственно, кортежем и словарём.
Система проверки типов не возражает, когда *args
и **kwargs
передают в таком виде:
foo(*args, **kwargs)
Но её не устраивает, когда передаются не все именованные аргументы:
foo(*args, key1=1) # error: Missing named argument "key2" for "foo"
Для того чтобы сделать все именованные аргументы необязательными, можно отключить флаг total
в определении класса, в котором используется TypedDict
:
# ...
class Kw(TypedDict, total=False):
key1: int
key2: str
# ...
Или можно, воспользовавшись typing.NotRequired
, указать на необязательность отдельных именованных аргументов:
# ...
class Kw(TypedDict):
key1: int
key2: NotRequired[str]
# ...
Это позволит передавать неполные наборы необязательных именованных аргументов, не вызывая при этом недовольство у системы проверки типов.
Вот и всё!
О, а приходите к нам работать? ???? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (2)
omaxx
15.01.2024 22:12Может быть пример
class Kw(TypedDict): key1: int key2: bool def foo(*args: Unpack[tuple[int, str]], **kwargs: Unpack[Kw]) -> None: ...
и показывает как можно аннотировать функцию, но он точно не отвечает на вопрос, а зачем так писать, когда можно записать проще:
def foo(arg1: int, arg2: str, /, kwarg1: int, kwarg2: bool): ...
ko_0n
А что mypy скажет в случае передачи key3? Вопрос риторический. В этом примере вообще нет пользы от TypedDict, ведь можно просто явно передать ключи и их типы. Можно было упомянуть namedtuple. И да, я понимаю, что это перевод, но это какой-то обрубок.