Аннотирование args и kwargs в Python

от автора

Когда я пытаюсь обойтись без *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 разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде


ссылка на оригинал статьи https://habr.com/ru/articles/786326/


Комментарии

Добавить комментарий

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