Аннотирование 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]


# ...

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

Вот и всё!

О, а приходите к нам работать?
Источник: https://habr.com/ru/companies/wunderfund/articles/786326/


Интересные статьи

Интересные статьи

Всем привет! Собрали для вас подборку вспомогательных материалов для изучения Python — они помогут разработчикам решать более сложные задачи и чувствовать себя увереннее в работе. Нам помога...
Дабы исчерпать до дна тему сокетов в Python я решил изучить все возможные способы их использования в данном языке. Чтобы всех их можно было испытать и попробовать на зуб, были созданы 19 версий просто...
Множество (Set) — структура данных, которая позволяет достаточно быстро (в зависимости от реализации) применить операции add, erase и is_in_set. Но иногда этого не достаточно: например,...
Никого не интересует, умеете ли вы разворачивать связанный список — всем нужно, чтобы можно было легко запускать ваши программы на их машине. Это становится возможным благодаря Docker. ...
В это статье я хочу показать пример того, как андроид устройство можно использовать для разработки на таких языках программирования как python с библиотекой opencv в среде VSCode (будет...