$ sudo teach IT
Модуль 6 · Функции · Урок 6.3

*args и **kwargs

Учимся писать функции, которые принимают любое количество аргументов — и разбираемся, что на самом деле скрывается за двумя звёздочками

Теория~30 минутНовичок*args**kwargsпорядок параметровраспаковка при вызове

Вы уже писали функции с обычными параметрами: def area(width, height):. Такая функция требует ровно два аргумента, не больше и не меньше. А что делать, если заранее не известно, сколько значений придёт — одно, три или десять? Добавлять параметры про запас неудобно и ненадёжно: рано или поздно кто-то передаст ещё одно значение, и функция сломается.

Python решает эту задачу двумя специальными обозначениями в заголовке функции: *args и **kwargs. С ними функция принимает сколько угодно позиционных и сколько угодно именованных аргументов. Так устроена, например, встроенная функция print() — ей можно передать один аргумент, а можно десять.

Разберём, что происходит внутри функции с такими аргументами, в каком порядке их можно комбинировать и как передавать их дальше, в другую функцию.

*args — произвольное число позиционных аргументов

Представьте почтовый ящик без ограничения по размеру: сколько бы писем вы ни отправили, все они туда попадут, а хозяин потом разберёт их по одному. Звёздочка перед именем параметра в заголовке функции работает похоже — она говорит Python: «все позиционные аргументы, которые не достались обычным параметрам, сложи в один ящик и отдай сюда».

Официально это называется упаковкой аргументов (packing). Параметр с одной звёздочкой в заголовке функции принято называть *args — это не ключевое слово, а просто договорённость программистов на Python (от английского «arguments»). Можно было бы написать *numbers или *items, и код бы работал точно так же, но чужой код будет читать сложнее: все привыкли именно к args.

def greet(*args):
    print(type(args))
    print(args)

greet("Привет")
greet("Привет", "Алиса")
greet("Привет", "Алиса", "!")

Разберём построчно, что здесь происходит:

  • def greet(*args): — функция принимает любое число позиционных аргументов; внутри функции все они будут собраны в один параметр args.
  • print(type(args)) — выведет <class 'tuple'>. Каким бы способом ни вызвали greet, внутри args всегда будет кортеж.
  • greet("Привет") — передан один аргумент, поэтому args станет кортежем из одного элемента: ('Привет',).
  • greet("Привет", "Алиса") — передано два аргумента, args будет равен ('Привет', 'Алиса').
  • greet("Привет", "Алиса", "!") — три аргумента — три элемента в кортеже: ('Привет', 'Алиса', '!').
Почему именно кортеж, а не список? Кортеж неизменяемый, и это не случайность: так Python сигнализирует, что аргументы нужно читать, а не менять внутри функции. При этом с ним работают все привычные вещи: перебор циклом for, обращение по индексу, функция len().

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

def total(*args):
    result = 0
    for num in args:
        result += num
    return result

print(total(1, 2, 3))
print(total(10, 20, 30, 40))
print(total())

По шагам:

  • result = 0 — переменная-накопитель, с неё начинаем считать сумму (так же, как делали в циклах раньше).
  • for num in args: — перебираем кортеж args точно так же, как перебирали бы список чисел.
  • result += num — на каждом шаге прибавляем текущее число к накопителю.
  • total(1, 2, 3) выведет 6, total(10, 20, 30, 40) — 100.
  • total() выведет 0: аргументов не передали, args стал пустым кортежем (), цикл ни разу не выполнился, и вернулось начальное значение накопителя.

**kwargs — произвольное число именованных аргументов

Теперь представьте анкету, где каждое поле подписано: «имя», «возраст», «город». Заранее не известно, сколько полей заполнит конкретный человек, но каждое значение подписано, поэтому перепутать их невозможно. Именно так работает **kwargs: две звёздочки перед именем параметра говорят Python собрать все именованные аргументы (вида ключ=значение), которые не достались обычным параметрам, в один словарь.

Имя kwargs — сокращение от «keyword arguments» (именованные аргументы), тоже просто общепринятое соглашение, а не требование языка.

def show_info(**kwargs):
    print(type(kwargs))
    print(kwargs)

show_info(name="Алиса", age=25)
show_info(city="Москва", country="Россия")

Что здесь происходит:

  • def show_info(**kwargs): — функция принимает любое число именованных аргументов; все они соберутся в параметр kwargs.
  • print(type(kwargs)) — выведет <class 'dict'>: внутри функции kwargs — обычный словарь.
  • show_info(name="Алиса", age=25) — вызов с двумя именованными аргументами. Внутри функции kwargs станет равен {'name': 'Алиса', 'age': 25}: ключ — это имя аргумента слева от знака =, значение — то, что справа.
  • show_info(city="Москва", country="Россия") — здесь kwargs будет {'city': 'Москва', 'country': 'Россия'}.
Почему словарь? Именованный аргумент — это пара «имя → значение», а словарь для таких пар подходит лучше всего. Получается симметрия: *args собирает позиционные аргументы в кортеж, **kwargs — именованные в словарь.

Раз kwargs — обычный словарь, переберём его так же, как в уроке про словари — через .items():

def print_profile(**kwargs):
    for key, value in kwargs.items():
        print(f"  {key}: {value}")

print_profile(name="Алиса", age=25, city="Москва")
  • for key, value in kwargs.items(): — .items() отдаёт пары ключ-значение, а for сразу раскладывает каждую пару в две переменные: key и value.
  • print(f" {key}: {value}") — для каждой пары печатает строку вида name: Алиса с отступом в начале.
  • Вызов выведет три строки, в том порядке, в каком аргументы передали при вызове: name: Алиса, age: 25, city: Москва.

Строгий порядок параметров в заголовке функции

У Python есть чёткое правило, в каком порядке разные виды параметров могут стоять в заголовке функции. Нарушите порядок — получите SyntaxError ещё до запуска программы, интерпретатор даже не станет пытаться выполнить код. Порядок такой:

Порядок параметров функции
обычные параметры
→
*args
→
именованные-только параметры
→
**kwargs
def order(a, b, *args, sep=", ", **kwargs):
    pass

order(1, 2, 3, 4, 5, sep=" | ", color="red")
# a=1, b=2, args=(3, 4, 5), sep=" | ", kwargs={'color': 'red'}

Как аргументы разошлись по параметрам:

  • a и b — обычные параметры, забирают первые два позиционных значения: 1 и 2.
  • *args забирает всё, что осталось из позиционных аргументов: 3, 4, 5 — получится кортеж (3, 4, 5).
  • sep=", " — параметр после *args. Такой параметр называют именованным-только (keyword-only): передать в него значение можно только по имени, не по позиции.
  • **kwargs заберёт все оставшиеся именованные аргументы, которых нет среди обычных параметров: color="red" попадёт в kwargs как {'color': 'red'}.
Ловушка: именованный-только параметр нельзя передать по позиции. order(1, 2, " | ") не задаст sep — строка " | " просто окажется третьим элементом args. Единственный способ — написать sep=" | " явно.

А вот так писать нельзя — порядок нарушен, и код даже не запустится:

def bad(a, **kwargs, *args):
    pass

# SyntaxError: параметр после **kwargs недопустим

Здесь **kwargs стоит раньше *args. Python требует, чтобы **kwargs всегда был последним параметром в заголовке — ни один параметр не может идти после него.

Когда *args и **kwargs работают вместе

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

def universal(*args, **kwargs):
    print("Позиционные:", args)
    print("Именованные:", kwargs)

universal(1, 2, name="Алиса", role="admin")
universal("hello")
universal()

Что выведет каждый вызов:

  • universal(1, 2, name="Алиса", role="admin"): args заберёт позиционные 1 и 2 в кортеж (1, 2), kwargs заберёт именованные в словарь {'name': 'Алиса', 'role': 'admin'}.
  • universal("hello"): один позиционный аргумент, поэтому args равен ('hello',), а именованных не было — kwargs станет пустым словарём {}.
  • universal(): вообще без аргументов — оба соберутся пустыми: () и {}.

В сигнатуре можно смешать обычные параметры, *args и **kwargs одновременно — так часто пишут функции-логгеры:

def log(level, **meta):
    details = ", ".join(f"{k}={v}" for k, v in meta.items())
    print(f"[{level}]", details)

log("INFO", user="Алиса", action="login")
  • ", ".join(f"{k}={v}" for k, v in meta.items()) — для каждой пары ключ-значение из meta собирается кусочек вида key=value, а ", ".join(...) склеивает все кусочки через запятую с пробелом.
  • log("INFO", user="Алиса", action="login"): level получает "INFO", а два именованных аргумента уходят в meta. Вывод: [INFO] user=Алиса, action=login.

Распаковка при вызове: те же звёздочки, обратная операция

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

def describe(name, age, city):
    print(f"{name}, {age} лет, {city}")

data = ["Алиса", 25, "Москва"]
describe(*data)

По шагам:

  • data = ["Алиса", 25, "Москва"] — обычный список из трёх значений.
  • describe(*data) — звёздочка перед data в вызове говорит Python: разложи список на отдельные аргументы. Это то же самое, что написать describe("Алиса", 25, "Москва") вручную.
  • Функция получит три отдельных значения в параметры name, age, city и выведет Алиса, 25 лет, Москва.

Так же работает двойная звёздочка со словарём — она распаковывает пары ключ-значение в именованные аргументы:

person = {"name": "Алиса", "age": 25, "city": "Москва"}
describe(**person)

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

Не перепутайте два случая с одинаковым символом: * в заголовке функции (def f(*args)) — собирает аргументы в кортеж. * при вызове (f(*data)) — распаковывает коллекцию обратно в отдельные аргументы. Символ один, но смотрите, где именно он стоит: в def или в скобках вызова.

Собрать и распаковать можно и в одной программе — это и есть паттерн «передача насквозь»: функция принимает произвольные аргументы через *args/**kwargs, а затем распаковывает их и передаёт дальше, в другую функцию:

def send_email(to, subject):
    print(f"Кому: {to}")
    print(f"Тема: {subject}")

def send_with_log(*args, **kwargs):
    print("Отправляем письмо...")
    send_email(*args, **kwargs)
    print("Готово")

send_with_log("alice@mail.ru", subject="Привет")

Что здесь происходит:

  • send_with_log("alice@mail.ru", subject="Привет") — обёртка соберёт позиционный аргумент в args = ("alice@mail.ru",), а именованный — в kwargs = {"subject": "Привет"}.
  • send_email(*args, **kwargs) — собранные args и kwargs тут же распаковываются обратно в отдельные аргументы для send_email. Получается тот же вызов, что и send_email("alice@mail.ru", subject="Привет").
  • Главная выгода: send_with_log не знает, какие параметры есть у send_email. Она добавляет своё поведение и передаёт всё, что получила, дальше без изменений.

Частые ошибки

Параметр после **kwargs

def f(**kwargs, a): — SyntaxError. **kwargs всегда должен быть последним параметром в заголовке функции, ничего не может идти после него.

Попытка обратиться к kwargs по номеру

kwargs[0] внутри функции вызовет KeyError: kwargs — словарь, а не список, у него нет позиции 0. Обращайтесь по ключу: kwargs["name"], или перебирайте через .items().

Именованный-только параметр передан по позиции

Если в заголовке def f(*args, sep=", "):, вызов f(1, 2, " | ") не задаст sep — строка " | " просто станет третьим элементом args. Нужно писать f(1, 2, sep=" | ").

Путаница между сбором и распаковкой

Одна и та же звёздочка в заголовке собирает аргументы, а при вызове — распаковывает коллекцию. Если забыть об этом, легко не понять, почему func(*my_list) передаёт несколько отдельных аргументов, а не один список.

Что важно запомнить

  • *args в заголовке функции собирает все лишние позиционные аргументы в кортеж.
  • **kwargs в заголовке функции собирает все лишние именованные аргументы в словарь.
  • Порядок параметров в заголовке строгий: обычные параметры → *args → именованные-только параметры → **kwargs. Нарушение — SyntaxError.
  • Имена args и kwargs — это соглашение, а не требование языка, но лучше его придерживаться, чтобы код было легко читать.
  • Те же звёздочки при вызове функции работают наоборот — распаковывают готовый список или словарь в отдельные аргументы: f(*lst), f(**dct).
  • Сбор и распаковка вместе дают паттерн «передача насквозь»: func(*args, **kwargs) — основа для функций-обёрток, которые ничего не знают о параметрах функции внутри.

Проверьте себя

6 вопросов

Сумма через *args

Напишите функцию sum_all(*args), которая принимает любое количество чисел и возвращает их сумму.

Если аргументов нет — верните 0. Например, sum_all(1, 2, 3) должна вернуть 6.

Профиль через **kwargs

Premium

HTML-тег из kwargs

Premium