Полезные идиомы Python для проектного кода

Распаковка, *args и **kwargs в сигнатурах и вызовах

Содержание курса

Объявление *args и **kwargs в сигнатуре функции

До этого момента * и ** работали на стороне вызова — разворачивали готовые коллекции в аргументы. Теперь переходим на другую сторону: в саму сигнатуру функции.

*args: ловушка для лишних позиционных аргументов

Когда вы пишете *args в параметрах функции, Python собирает все позиционные аргументы, которые не «достались» конкретным параметрам, в один кортеж — и кладёт его в переменную args.

def summarize(label, *args):
    print(f"Label: {label}")
    print(f"Values: {args}")
    print(f"Type of args: {type(args)}")

summarize("scores", 90, 85, 78)
# Label: scores
# Values: (90, 85, 78)
# Type of args: <class 'tuple'>

label получил первый аргумент "scores", всё остальное ушло в args. Внутри функции args — обычный кортеж, его можно итерировать, передавать в len, суммировать через sum(args). Ничего особенного — просто кортеж.

Имя args — лишь соглашение, принятое в сообществе. Синтаксис держится на звёздочке, а не на имени:

def summarize(label, *values):  # работает так же
    print(sum(values))

Но отходить от args без причины не стоит: читатель привык к этому имени и сразу понимает, что перед ним переменное число позиционных аргументов.

**kwargs: ловушка для лишних именованных аргументов

Аналогично, **kwargs в сигнатуре собирает все именованные аргументы, которые не совпали ни с одним явным параметром, — и кладёт их в словарь {имя: значение}.

def configure(**kwargs):
    print(f"Type of kwargs: {type(kwargs)}")
    for key, value in kwargs.items():
        print(f"  {key} = {value}")

configure(host="localhost", port=5432, debug=True)
# Type of kwargs: <class 'dict'>
#   host = localhost
#   port = 5432
#   debug = True

Внутри функции kwargs — обычный словарь. Можно делать kwargs.get("port", 80), проверять "debug" in kwargs, передавать его дальше. Двойная звёздочка — только синтаксис объявления; дальше это просто dict.

Контракт порядка параметров

Python жёстко регламентирует, в каком порядке параметры могут идти в сигнатуре:

  1. Позиционные параметры (обычные, с дефолтом или без)
  2. *args
  3. Параметры, передаваемые только по имени (keyword-only)
  4. **kwargs

Любое нарушение этого порядка — SyntaxError ещё до запуска программы:

def bad(**kwargs, *args):  # SyntaxError
    pass

Важное следствие: параметры, объявленные после *args, автоматически становятся keyword-only. Передать их позиционно уже невозможно — все позиционные аргументы к тому моменту уже поглощены *args.

def process(*args, verbose):
    if verbose:
        print(args)

process(1, 2, 3, verbose=True)   # OK
process(1, 2, 3, True)            # TypeError: process() takes 0 positional arguments...

Во втором вызове True уходит в args, и параметр verbose остаётся незаполненным — Python не знает, что вы имели в виду именно его.

Если keyword-only параметру нужен дефолт, он объявляется прямо в сигнатуре:

def process(*args, verbose=False):
    if verbose:
        print(args)

Тогда process(1, 2, 3) работает без ошибки — verbose берёт значение по умолчанию.

Этот порядок — не произвол. Python нужно однозначно разграничить, какой аргумент куда идёт. Если **kwargs стоял бы перед *args, интерпретатор не смог бы понять, где заканчиваются именованные аргументы и начинаются позиционные.