Урок курса
Распаковка, *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 жёстко регламентирует, в каком порядке параметры могут идти в сигнатуре:
- Позиционные параметры (обычные, с дефолтом или без)
*args- Параметры, передаваемые только по имени (keyword-only)
**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 после выполнения?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
