Звёздочка в сигнатуре функции собирает лишние аргументы в одну переменную. Та же звёздочка на вызове делает обратное: разбирает готовый список по отдельным аргументам. Две противоположные роли у одного знака: отсюда почти вся путаница в теме. Код выполнен на CPython 3.14.5.
Один вызов — два набора аргументов
*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 звёздочки идут вместе с сигнатурами и обёртками. Проверить себя можно на задаче про декоратор-счётчик вызовов: обёртка обязана сохранить результат исходной функции, а значит принять и передать дальше любые аргументы.