Урок курса

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

Python для продвинутых: ООП, типизация и тестирование

В предыдущем уроке мы работали с zip, enumerate и sorted — функциями, которые возвращают итерируемые объекты. Теперь разберём механизм, который стоит за тем, как Python вообще умеет «раскладывать» итерируемое по переменным — и как этим управлять через звёздочку.

Распаковка итерируемого в присваивании: базовый синтаксис и *rest

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

point = (10, 20, 30)
x, y, z = point
print(x, y, z)  # 10 20 30

Работает с любым итерируемым: кортежем, списком, строкой, результатом zip — чем угодно, что можно обойти циклом.

Проблема возникает, когда количество элементов заранее неизвестно или может меняться. Тут появляется звёздочный синтаксис.

first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

*rest собирает всё, что не «досталось» конкретным переменным, и упаковывает это в список — не в кортеж, именно в список. Звёздочная переменная может стоять не только в конце:

first, *middle, last = [1, 2, 3, 4, 5]
print(first)   # 1
print(middle)  # [2, 3, 4]
print(last)    # 5

Пython сам вычисляет, сколько элементов уйдёт в «звёздочную» переменную, ориентируясь на количество конкретных переменных вокруг.

Одно жёсткое ограничение: звёздочная переменная в одном присваивании может быть только одна. *a, *b = [1, 2, 3] — это SyntaxError. Python не умеет делить остаток между двумя «сборщиками».

Что происходит при несовпадении числа переменных

Если звёздочки нет, а количество переменных не совпадает с количеством элементов — получаем ValueError:

# Слишком много значений
a, b = [1, 2, 3]  # ValueError: too many values to unpack

# Слишком мало значений
a, b, c = [1, 2]  # ValueError: not enough values to unpack

Звёздочка страхует от этой ошибки, но только от одной стороны: если конкретных переменных больше, чем элементов, ValueError всё равно возникнет.

first, second, *rest = [1]  # ValueError: not enough values to unpack

Здесь Python пытается отдать по одному элементу first и second, но элемент только один — rest тут уже ничего не поможет.

Этот механизм — основа для понимания того, как * работает при вызовах функций и в их сигнатурах. Но об этом в следующих секциях.

Операторы * и ** при вызове функции

В присваивании * собирал остаток в список. При вызове функции тот же символ работает в обратную сторону: берёт уже готовый список или кортеж и разворачивает его в отдельные позиционные аргументы.

def add(a, b, c):
    return a + b + c

numbers = [10, 20, 30]
result = add(*numbers)
print(result)  # 60

Пython видит *numbers и ведёт себя так, как если бы вы написали add(10, 20, 30) вручную. Это удобно, когда аргументы уже лежат в коллекции и переписывать вызов поэлементно незачем.

Работает с любым итерируемым: списком, кортежем, результатом range, генератором. Можно даже комбинировать несколько *-распаковок в одном вызове:

first = [1, 2]
second = [3, 4]
print(*first, *second)  # 1 2 3 4

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

Оператор со словарём

** делает то же самое, но для именованных аргументов. Словарь разворачивается в пары имя=значение:

def greet(name, greeting):
    print(f"{greeting}, {name}!")

params = {"name": "Alice", "greeting": "Hello"}
greet(**params)  # Hello, Alice!

Порядок ключей в словаре не важен — Python сопоставляет их с параметрами функции по имени, а не по позиции.

Как и *, оператор ** можно использовать несколько раз в одном вызове:

defaults = {"greeting": "Hi"}
overrides = {"name": "Bob"}
greet(**defaults, **overrides)  # Hi, Bob!

Если один и тот же ключ встречается в двух словарях при **-распаковке, Python бросает TypeError: got multiple values for keyword argument.

Ограничение на ключи при распаковке

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

bad = {1: "value"}
def f(**kwargs): pass

f(**bad)  # TypeError: keywords must be strings

Python не умеет отобразить 1 на имя параметра — имена параметров всегда строки. Это фундаментальное требование языка, а не особенность конкретной функции.

Ещё один момент: ключи словаря должны совпадать с именами параметров функции (если функция не объявлена с **kwargs, которая поглощает всё). Лишний ключ — TypeError: got an unexpected keyword argument.

Оба оператора — * и ** — можно комбинировать в одном вызове:

def report(title, *items, sep=","):
    print(title + sep + sep.join(items))

args_list = ["item1", "item2"]
kw = {"sep": "-"}
report("Header", *args_list, **kw)  # Header-item1-item2

Здесь *args_list разворачивается в позиционные аргументы, а **kw — в именованный. Вместе они позволяют собрать вызов функции из заранее подготовленных структур данных, не переписывая его строку вручную.

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

Совместное использование *args, keyword-only параметров и **kwargs

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

def report(*args, sep=' ', **kwargs):
    print(f"Values: {args}")
    print(f"Separator: {sep!r}")
    print(f"Extra options: {kwargs}")

Здесь три зоны ответственности:

  • *args поглощает все позиционные аргументы, которые передали при вызове;
  • sep — keyword-only параметр с дефолтом ' ': стоит после *args, поэтому передать его позиционно невозможно;
  • **kwargs собирает всё, что не попало ни в args, ни в sep.

Проверим несколько вызовов:

report('a', 'b', 'c')
# Values: ('a', 'b', 'c')
# Separator: ' '
# Extra options: {}

report('a', 'b', sep='-', color='red', size=12)
# Values: ('a', 'b')
# Separator: '-'
# Extra options: {'color': 'red', 'size': 12}

report(sep='|')
# Values: ()
# Separator: '|'
# Extra options: {}

Во втором вызове 'a' и 'b' ушли в args, sep='-' попал точно в keyword-only параметр, а color и size — в kwargs. Никаких конфликтов: Python чётко разграничивает, куда что идёт, следуя порядку параметров в сигнатуре.

Третий вызов показывает, что все три зоны могут быть пустыми одновременно — и это нормально. args становится пустым кортежем (), kwargs — пустым словарём {}.

Как это работает, когда вызов сам строится из коллекций

Практически часто бывает так: и аргументы, и опции уже лежат в переменных. Тут * и ** на стороне вызова соединяются с *args/**kwargs на стороне определения:

positional = ('x', 'y', 'z')
options = {'color': 'blue', 'bold': True}

report(*positional, sep='/', **options)
# Values: ('x', 'y', 'z')
# Separator: '/'
# Extra options: {'color': 'blue', 'bold': True}

*positional разворачивается в три позиционных аргумента, они оседают в args. sep='/' передаётся именованно и попадает в keyword-only параметр. **options разворачивается в два именованных аргумента, которые не совпадают ни с каким явным параметром — и оба уходят в kwargs.

Передача kwargs дальше по цепочке

Один из типичных паттернов — функция-обёртка, которая принимает **kwargs и пробрасывает их в другую функцию:

def styled_report(*args, **kwargs):
    # Извлекаем то, что нам нужно самим
    prefix = kwargs.pop('prefix', '')
    # Остаток передаём дальше
    report(*args, **kwargs)
    if prefix:
        print(f"[{prefix}]")

styled_report('a', 'b', sep='-', prefix='INFO', color='green')
# Values: ('a', 'b')
# Separator: '-'
# Extra options: {'color': 'green'}
# [INFO]

kwargs.pop('prefix', '') забирает prefix из словаря до того, как он уйдёт дальше. Остаток {'color': 'green'} летит в report через **kwargs. Это чистый способ расширить функцию, не ломая её контракт с вызывающей стороной.

Именно такой паттерн — *args + **kwargs в __init__ — появится в следующем уроке при работе с классами, где конструктор должен принимать произвольный набор настроек и часть из них передавать родительскому классу.

Попробуйте решить

Дан код:

data = [10, 20, 30, 40, 50]
first, *middle, last = data

Чему равны first, middle и last после выполнения?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку