*args и **kwargs в Python: что это и как работает

Python Автор: Среда и версия: CPython 3.14.5
содержание

Звёздочка в сигнатуре функции собирает лишние аргументы в одну переменную. Та же звёздочка на вызове делает обратное: разбирает готовый список по отдельным аргументам. Две противоположные роли у одного знака: отсюда почти вся путаница в теме. Код выполнен на CPython 3.14.5.

Python / 01

Один вызов — два набора аргументов

Один вызов — два набора аргументов01 / вход 02 / операция 03 / результат f(1, 2, mode="fast") *args (1, 2) тот же вызов **kwargs {"mode": "fast"}01 / вход02 / операция03 / результатf(1, 2,mode="fast")*args(1, 2)тот же вызов**kwargs{"mode": "fast"}
*args собирает позиционные аргументы в кортеж, а **kwargs — именованные в словарь.

Что собирает звёздочка в сигнатуре

Одна звезда складывает все лишние позиционные аргументы в кортеж:

def total(*args):
    print(args, type(args).__name__, len(args))

total(1, 2, 3)
total()
(1, 2, 3) tuple 3
() tuple 0

Пустой вызов даёт пустой кортеж, а не ошибку и не None. Проверять на None тут нечего, if args: достаточно.

Две звезды складывают именованные аргументы в словарь:

def show(**kwargs):
    print(kwargs, type(kwargs).__name__)

show(host="localhost", port=5432)
show()
{'host': 'localhost', 'port': 5432} dict
{} dict

Имена args и kwargs держатся на соглашении, а не на синтаксисе. Работает только звезда:

def f(*items, **opts):
    print(items, opts)

f(1, 2, mode="fast")
(1, 2) {'mode': 'fast'}

Соглашение всё же соблюдай. Чужой код читают глазами, и *args узнаётся мгновенно.

В каком порядке идут аргументы

Полная сигнатура выстраивается от самого конкретного к самому общему: обычные, *args, именованные с умолчанием, **kwargs.

def f(a, b=2, *args, key=None, **kwargs):
    print(a, b, args, key, kwargs)

f(1)
f(1, 20, 30, 40, key="k", extra=9)
1 2 () None {}
1 20 (30, 40) k {'extra': 9}

Во втором вызове 20 село в b, потому что оно ближе к началу, и только остаток ушёл в args. Всё, что стоит после *args, передаётся исключительно по имени: позиционному аргументу туда уже не добраться.

Почему на вызове звезда делает обратное

В сигнатуре звезда собирает, на вызове раскладывает:

def point(x, y, z):
    print(x, y, z)

coords = [1, 2, 3]
point(*coords)

conf = {"x": 7, "y": 8, "z": 9}
point(**conf)
1 2 3
7 8 9

Список разошёлся по трём параметрам, словарь — по именам. Без звезды тот же список приезжает одним аргументом:

def point(x, y, z):
    print(x, y, z)

coords = [1, 2, 3]
try:
    point(coords)
except TypeError as exc:
    print(f"TypeError: {exc}")
TypeError: point() missing 2 required positional arguments: 'y' and 'z'

Сообщение сбивает с толку: аргумент вроде передан, а Python говорит, что двух не хватает. Он прав, coords целиком занял x.

Раскладывается любой итерируемый объект, в том числе строка, и вот это уже ловушка:

def f(*args):
    print(args)

f(*"abc")
f("abc")
('a', 'b', 'c')
('abc',)

Строка распалась на символы. Тот же дефект встречается в Thread(args=("/pricing")): без запятой это не кортеж, а строка, и она разъедется по буквам.

Что означают голая звезда и слэш

Звезда без имени закрывает всё, что после неё, от позиционной передачи:

def connect(host, *, timeout=5):
    print(host, timeout)

connect("db", timeout=1)
try:
    connect("db", 1)
except TypeError as exc:
    print(f"TypeError: {exc}")
db 1
TypeError: connect() takes 1 positional argument but 2 were given

Приём стоит того, чтобы им пользоваться. Вызов connect("db", 1) читается как загадка, connect("db", timeout=1) — как предложение.

Слэш работает зеркально: до него аргументы передаются только позиционно.

def area(w, h, /, unit="см"):
    print(w, h, unit)

area(3, 4)
try:
    area(w=3, h=4)
except TypeError as exc:
    print(f"TypeError: {exc}")
3 4 см
TypeError: area() got some positional-only arguments passed as keyword arguments: 'w, h'

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

Зачем всё это декораторам

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

def log(func):
    def wrapper(x):
        return func(x)
    return wrapper

@log
def add(a, b):
    return a + b

try:
    add(1, 2)
except TypeError as exc:
    print(f"TypeError: {exc}")
TypeError: log.<locals>.wrapper() takes 1 positional argument but 2 were given

Пара «собрать и разложить» решает задачу целиком:

def log(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log
def add(a, b=0):
    return a + b

print(add(1, 2), add(1, b=5))
3 6

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

Ошибки, на которых спотыкаются

Один аргумент дважды. Позиционный и именованный могут попасть в один параметр, и Python это не прощает:

def greet(name, greeting="привет"):
    print(greeting, name)

try:
    greet("Аня", name="Борис")
except TypeError as exc:
    print(f"TypeError: {exc}")
TypeError: greet() got multiple values for argument 'name'

Нестроковые ключи в **kwargs. Словарь раскладывается по именам параметров, а имя не может быть числом:

def f(**kwargs):
    print(kwargs)

try:
    f(**{1: "a"})
except TypeError as exc:
    print(f"TypeError: {exc}")
TypeError: keywords must be strings

Забытая запятая в кортеже из одного элемента. ("/pricing") даёт строку, а кортеж получается только с запятой: ("/pricing",). Разница вылезет при первой же распаковке.

*args вместо явных параметров. Функция, принимающая что угодно, не подсказывает вызывающему ничего: ни имён, ни количества. Бери звёздочки там, где число аргументов действительно заранее неизвестно, а не чтобы не думать над сигнатурой.

Частые вопросы

Чем *args отличается от **kwargs

*args собирает позиционные аргументы в кортеж, **kwargs — именованные в словарь. Кортеж неизменяем, поэтому дописать в args внутри функции не выйдет, только собрать новый.

Можно ли назвать их иначе

Да, синтаксис держится на звёздочках. *items и **opts работают точно так же, но args и kwargs узнаются с первого взгляда.

Как передать список в функцию, которая ждёт отдельные аргументы

Звездой на вызове: point(*coords). Без неё список приедет одним аргументом и функция упадёт с жалобой на нехватку остальных.

Что значит одинокая звезда в сигнатуре

Всё, что стоит после неё, передаётся только по имени. Так закрывают от позиционного вызова необязательные флаги, чтобы f(x, True, False) не появилось в чужом коде.

Как слить два словаря

Двойной звездой прямо в литерале:

base = {"host": "localhost", "port": 5432}
over = {"port": 6432, "ssl": True}
print({**base, **over})
{'host': 'localhost', 'port': 6432, 'ssl': True}

Побеждает последний: port взялся из over.

Где потренироваться

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

Источники