*args и **kwargs: две звёздочки, которые стоят по-разному — и одна из них делает неизвестные имена допустимыми
*args — кортеж, **kwargs — словарь, и на этом сходство кончается. На передаче звёздочка почти бесплатна, а две дороже вчетверо. А **kwargs, дописанный «для гибкости», превращает опечатку в имени аргумента из TypeError в молча принятое значение по умолчанию — и заодно выбрасывает подсказку, которой интерпретатор научился в 3.13.
Полное техническое изложение
TL;DR
Звёздочка делает противоположные вещи на двух сторонах вызова. В
объявлении она собирает: одна — лишние позиционные аргументы в кортеж
args, две — лишние именованные в словарь kwargs. В вызове она
раскладывает: одна — последовательность в отдельные позиционные аргументы,
две — словарь в отдельные именованные. Собранные объекты функция получает
даже когда передавать нечего: при вызове без аргументов внутрь приходят () и
{}, а не None.
Отсюда главное следствие: две звёздочки, дописанные в объявление «для
гибкости», делают неизвестные имена именованных аргументов допустимыми для этой
функции — их теперь есть куда положить. Вызов connect("db", timeuot=5) перестаёт быть
TypeError и молча уходит в options, а timeout остаётся равным 30. Второе
следствие — цена, и она у двух звёздочек разная: plain(*args) почти
бесплатен (×1,1–1,3 от прямого вызова), plain(**kwargs) дороже вчетверо и
больше (×4,0–5,2). Устойчиво на всех четырёх версиях; три слоя декораторов —
×8,5–11,9, то есть восемь девятых времени уходит не на работу, а на подход к
ней.
Дальше — числа, версии и границы. () и {} — это семантика, а не
утверждение о выделении памяти: пустой кортеж в CPython один на всех, и
сколько при этом выделяется — вопрос к замеру ниже, а не к языку. Допустимыми
становятся ровно неизвестные имена, а остальные правила связывания никуда не
деваются — прогон bench/args-kwargs/binding_rules.py проверяет каждое
настоящим вызовом:
strict(a=1, unknown=9) TypeError
loose(a=1, unknown=9) a=1 b=2 kwargs={'unknown': 9}
loose() TypeError ← обязательный параметр
loose(1, a=2) TypeError ← дважды переданный
positional_only_loose(a=1, b=2, c=3) TypeError ← имя вместо позиции
Последняя строка объясняет, почему / в объявлении — не украшение. А самый
неожиданный случай прогон печатает отдельно: у функции def f(a, /, b, *, c, **kwargs) вызов f(1, b=2, c=3, a=99) не ошибка — a=99 уходит в kwargs,
потому что позиционно-только параметр своё имя для ключевых аргументов не
занимает.
Хуже того: в 3.13 интерпретатор научился подсказывать правильное имя — Did you
mean 'timeout'?
(Вы имели в виду 'timeout'?). Функция с **kwargs эту помощь выбрасывает и на 3.14
ведёт себя так же, как на 3.11.
/ из PEP 570 нужен не для красоты: пока параметр можно передать по имени, это
имя занято, и ключ с таким же названием в **kwargs не попадёт. Так объявлен
сам dict: его сигнатура — ($self, /, *args, **kwargs).
- как объявляют функцию с параметрами и как её потом вызывают;
- разницу между «передать по порядку» и «передать по имени»:
f(1, 2)иf(a=1, b=2); - что список и словарь — это контейнеры, и словарь хранит пары «имя — значение».
co_argcount,co_kwonlyargcount,CALL_FUNCTION_EX,BUILD_MAP,DICT_MERGE;- зачем в сигнатуре бывают
/и голая*,functools.wraps,Unpack[TypedDict].
База: звёздочка на двух сторонах вызова
У звёздочки в Python несколько ролей, и все они пишутся одинаково — одним и тем
же символом. Пока роли не разведены, разговор про *args и **kwargs ходит по
кругу; разведём их сразу.
Различает роли сторона: там, где функцию объявляют, звёздочка значит одно, а там, где её вызывают, — противоположное.
В объявлении звёздочка собирает. Она берёт то, чему не нашлось отдельного параметра, и кладёт под одно имя:
- одна звёздочка собирает лишние позиционные аргументы — те, что переданы по порядку;
- две звёздочки собирают лишние именованные — те, что переданы по имени.
В вызове звёздочка раскладывает. Она берёт готовый контейнер и подставляет его содержимое как отдельные аргументы:
- одна раскладывает последовательность в позиционные аргументы:
f(*seq)— это то же самое, чтоf(seq[0], seq[1], …); - две раскладывают словарь в именованные:
f(**mapping)— этоf(имя=значение, …)по каждой паре.
Вот те же роли рядом, а последней строкой — запись, которая звёздочку использует совсем иначе:
| запись | где стоит | что делает |
|---|---|---|
def f(*args, **kwargs) | объявление | упаковывает лишние аргументы в кортеж и словарь |
f(*seq) | вызов | распаковывает последовательность в позиционные аргументы |
f(**mapping) | вызов | распаковывает отображение в именованные аргументы |
def f(a, /, b, *, c) | объявление | ничего не упаковывает: / и голая * только делят параметры на виды |
Четыре разные операции, и путаница между первой и второй — самая частая:
f(*args) внутри обёртки не создаёт кортеж, а разбирает уже созданный.
Последняя строка к сборке отношения не имеет вовсе, и до неё мы дойдём
отдельно.
И вот вопрос, на который отвечает остальной урок: чем за эту сборку платят? Записывается она одним символом и выглядит бесплатной — но у неё есть и цена во времени, и последствие, которое временем не измеряется вовсе.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, что собранное приходит кортежем и словарём, а не чем попало; что две звёздочки на передаче стоят вчетверо дороже одной; и что имя, которого у функции нет, перестаёт быть ошибкой, как только в объявлении появились две звёздочки.
Механизм 1: что это за объекты
*args — кортеж, **kwargs — словарь, и правила связывания аргументов от версии не зависят.Начнём с того, что проверяется одной функцией.
def collect(*args, **kwargs):
return type(args).__name__, type(kwargs).__name__collect(1, 2, x=3) возвращает ('tuple', 'dict'). Не список — кортеж, и
это проверяется одной строкой: args.append(3) даёт
AttributeError: 'tuple' object has no attribute 'append'. Изменяемого
контейнера здесь не будет, сколько бы аргументов ни пришло.
Второе и менее очевидное: объекты создаются даже когда передавать нечего.
def probe(*args, **kwargs):
return args, kwargs
probe() # -> ((), {})Не None, а пустой кортеж и пустой словарь. Справочник языка говорит именно
так: *identifier is initialized to a tuple receiving any excess positional
parameters, defaulting to the empty tuple
(инициализируется кортежем, принимающим все лишние позиционные параметры, по умолчанию пустым кортежем). Поэтому if args: — правильная
проверка, а if args is not None: — всегда истина.
Что о звёздочках знает сам код функции
def full(a, b=2, *args, c, d=4, **kwargs):
passОбъявление выглядит запутанным, но интерпретатор раскладывает его на три независимых числа и два флага:
| поле | значение | что означает |
|---|---|---|
co_argcount | 2 | a и b — обычные: и позиционно, и по имени |
co_posonlyargcount | 0 | до / ничего нет |
co_kwonlyargcount | 2 | c и d — только по имени |
CO_VARARGS | True | есть *args |
CO_VARKEYWORDS | True | есть **kwargs |
Отсюда простое следствие, которое снимает главную путаницу: c и d стали
только-именованными не потому, что у них есть значения по умолчанию, а потому
что они стоят после *args. Это правило из PEP 3102, и голая * в
объявлении делает то же самое без сбора кортежа.
Порядок именованных аргументов сохраняется: order(z=1, a=2, m=3) даёт ключи в
порядке ['z', 'a', 'm'], а не отсортированные.
Механизм 2: ошибка, которая не падает
Функция подключения. Три понятных параметра, и кто-то дописал четвёртый — «мало ли что понадобится прокинуть в драйвер».
def connect(host, port=5432, timeout=30, **options):
...Через полгода в другом файле пишут вызов:
connect("db", timeuot=5)Ошибки не будет. timeuot — законный ключ для **options, и функция получит
timeout = 30, а пятёрка ляжет в options, куда никто не смотрит. Соединение
установится, программа продолжит работу, и разница проявится однажды под
нагрузкой — таймаутом, которого не ждали.
Без **options тот же вызов падает сразу, и падает информативно.
Почему это стало хуже, а не лучше
В 3.13 интерпретатор научился подсказывать правильное имя. «Что нового»
говорит: The error message now tries to suggest the correct keyword argument
when an incorrect keyword argument is passed to a function
(Сообщение об ошибке теперь пытается подсказать правильный именованный аргумент, если в функцию передали неверный). Проверено на всех
четырёх версиях:
| версия | connect("db", timeuot=5) без **options |
|---|---|
| 3.11.15 | got an unexpected keyword argument 'timeuot' |
| 3.12.3 | то же |
| 3.13.7 | ... 'timeuot'. Did you mean 'timeout'? |
| 3.14.7 | то же, что 3.13.7 |
То есть язык стал ловить ровно эту опечатку лучше — а функция, объявленная с
**kwargs, эту помощь выбрасывает. С ней 3.14 ведёт себя так же, как 3.11:
молчит.
Чем это лечится
Не внимательностью. Если **kwargs действительно нужен — проверкой неизвестных
ключей там же, где они принимаются:
KNOWN = {"sslmode", "application_name"}
def connect(host, port=5432, timeout=30, **options):
unknown = set(options) - KNOWN
if unknown:
raise TypeError(f"неизвестные параметры: {', '.join(sorted(unknown))}")Четыре строки возвращают отказ по неизвестному имени — тот самый, который
перестал возникать сам, как только имена стали допустимыми. А если **kwargs
нужен не был — его отсутствие и есть проверка, причём бесплатная.
Механизм 3: / и голая * — зачем занимать имена
Две вещи из PEP 570 и PEP 3102, у которых есть измеримое следствие, а не только косметическое.
Голая * запрещает передавать всё, что после неё, позиционно:
def open_conn(host, *, timeout=30, retries=3):
...Теперь open_conn("db", 5) — ошибка, а не таймаут в пять секунд, попавший в
timeout по счёту. Порядок параметров перестаёт быть частью договора, и его
можно менять, не ломая вызывающих.
Слеш / делает обратное — запрещает передавать по имени. И нужен он не для
симметрии, а по причине, которую видно только на запуске:
def name_taken(name, **kwargs):
return name, kwargs
def name_free(name, /, **kwargs):
return name, kwargs| вызов | результат |
|---|---|
name_taken("a", name="b") | TypeError: got multiple values for argument 'name' |
name_free("a", name="b") | ('a', {'name': 'b'}) |
Пока параметр можно передать по имени, это имя занято: ключ с таким же
названием в **kwargs попасть не может. PEP 570 называет ровно этот случай: A
key scenario is when a function accepts any keyword argument but can also
accepts a positional one
(Ключевой случай — когда функция принимает любой именованный аргумент, но при этом принимает и позиционный).
Так объявлен и сам dict. Его сигнатура видна прямо из интерпретатора:
dict.__init__.__text_signature__ # ($self, /, *args, **kwargs)
dict(self="x") # {'self': 'x'}Слеш освобождает имя self — без него dict(self="x") давало бы
got multiple values for argument 'self'.
Механизм 4: что обёртка делает с сигнатурой
У *args, **kwargs в декораторе есть побочный эффект, которого не видно в коде
обёртки.
def wrap(fn):
def inner(*args, **kwargs):
return fn(*args, **kwargs)
return innerЗдесь wrap — декоратор, а inner — обёртка, которую он возвращает и которая
встаёт на место исходной функции. Сигнатуру теряет именно обёртка: у декоратора
wrap она своя и никуда не девается.
inspect.signature у такой обёртки показывает (*args, **kwargs) — имена
параметров исчезли, а вместе с ними подсказки редактора и половина пользы от
проверки типов. Лечится это @functools.wraps(fn), и после него сигнатура
показывается исходная: (host, port=5432, timeout=30).
Но она не описывает то, что обёртка готова принять. Проверено: обёртка пропускает и пять позиционных аргументов, и любое имя — не
проверив ничего. Падает вызов уже внутри: connect() takes from 1 to 3 positional arguments but 5 were given. @wraps возвращает сигнатуру для
инструментов, но не заставляет обёртку ей следовать.
Видно это и в трассировке: кадров на один больше, чем слоёв, — свой кадр вызова плюс по кадру на каждую обёртку.
| как вызвано | кадров в трассировке |
|---|---|
| напрямую | 1 |
| через одну обёртку | 2 |
| через три | 4 |
Само сообщение всегда называет внутреннюю функцию — @wraps скопировал ей имя,
и различить их по тексту нельзя. Различаются кадры: без @wraps в трассировке
подряд стоят три inner, и до строки с опечаткой читать три чужих кадра.
Механизм 5: типизация — чем вернуть стёртые имена
Это единственный автоматический способ вернуть то, что **kwargs стирает.
До 3.12 аннотировать **kwargs можно было только одним типом на все значения.
PEP 692 называет это ограничение прямо: Currently
(Сейчас **kwargs can be type
hinted as long as all of the keyword arguments specified by them are of the
same type. However, that behaviour can be very limiting**kwargs можно аннотировать, только если все передаваемые через них именованные аргументы одного типа. Но такое поведение может сильно ограничивать). С 3.12 появился
Unpack[TypedDict]:
class Options(TypedDict, total=False):
sslmode: str
application_name: str
def connect(host: str, **options: Unpack[Options]) -> None:
...Теперь проверяющий типы — mypy, pyright — снова видит имена и ловит
timeuot=5 до запуска: error: Unexpected keyword argument "timeuot". На сам
вызов это не влияет: интерпретатор по-прежнему принимает его молча — для него
это имя допустимо. Отказ по неизвестному имени возвращается снаружи, до
запуска, раз внутри вызова его больше нет.
Глубже: сколько стоят звёздочки
Семь форм вызова, измеренные подряд одним процессом: четыре — способы передать
plain два числа, три — то же самое через функцию со звёздочками и через
обёртки.
Главное здесь — асимметрия. Две звёздочки пишутся рядом, называются одним дыханием и стоят совершенно по-разному:
plain(*args)— ×1,1–1,3 от прямого позиционного вызова. Практически бесплатно.plain(**kwargs)— ×4,0–5,2. Вчетверо и больше.
Причина видна в байт-коде, и она конкретнее, чем «словарь сложнее кортежа». Одинаково на всех четырёх версиях:
plain(*ARGS) LOAD_GLOBAL LOAD_GLOBAL CALL_FUNCTION_EX
plain(**KWARGS) LOAD_GLOBAL LOAD_CONST BUILD_MAP LOAD_GLOBAL DICT_MERGE CALL_FUNCTION_EX
На стороне вызова кортеж уходит как есть: CALL_FUNCTION_EX получает
готовый объект. Внутри функция с *args соберёт уже свой кортеж — g(*T) is T
даёт False, — но это происходит и при обычном вызове. А перед вызовом со
словарём интерпретатор
создаёт новый словарь (BUILD_MAP) и копирует в него переданный
(DICT_MERGE) — то есть словарь копируется на каждом вызове, ещё до того, как
начнётся сопоставление имён с параметрами. Отсюда и четырёхкратная разница.
Именованная передача устроена иначе и стоит дешевле: она компилируется не в
CALL_FUNCTION_EX, а в CALL_KW с готовым кортежем имён в константах —
словаря не создаётся вовсе. Отсюда и разрыв: ×1,3–1,9 против ×4 с лишним у
plain(**kwargs). Это не довод против именованных аргументов —
читаемость дороже десятка наносекунд (на 3.13.7 разница вышла 11,3 нс), — но довод против того, чтобы считать
их бесплатными в горячем цикле.
Обёртки складываются
Декоратор на *args, **kwargs делает обе работы сразу: принимает звёздочками
(собирает кортеж и словарь) и передаёт звёздочками (разбирает их обратно).
| сколько слоёв | во сколько раз дороже прямого вызова |
|---|---|
| один | ×3,6–4,6 |
| три | ×8,5–11,9 |
На трёх слоях восемь девятых времени уходит не на работу, а на подход к ней. Это не аргумент против декораторов — это аргумент за то, чтобы знать, где именно они стоят дорого: в обработчике, который ходит в базу, эти сотни наносекунд не видны совсем; в функции, вызываемой миллион раз за проход, они становятся заметной долей.
История версий
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| 3.0 | PEP 3102 даёт голую * и правило: всё после неё передаётся только по имени. Мотив записан прямо — функция с переменным числом аргументов, у которой есть ещё и «опции»; до этого их доставали из **kwargs руками. Отсюда же co_kwonlyargcount — отдельное поле в коде функции. | |
| 3.8 | PEP 570 даёт /. Нужен он не для симметрии: пока параметр передаётся по имени, это имя занято и ключ с таким же названием в **kwargs не попадёт. Проверено запуском: name_taken("a", name="b") — TypeError, name_free("a", name="b") — работает. | |
| 3.12 | PEP 692 разрешил Unpack[TypedDict] для **kwargs: проверяющий снова видит имена и типы. До этого все значения обязаны были иметь один тип — that behaviour can be very limiting (такое поведение может сильно ограничивать). Само имя Unpack появилось раньше, в 3.11 (PEP 646), поэтому такую аннотацию принимают и на более старом целевом уровне. | |
| 3.13 | Интерпретатор подсказывает правильное имя: (Вы имели в виду 'timeout'?). Проверено на всех четырёх версиях — на 3.11 и 3.12 подсказки нет. Важная оговорка: функцию с **kwargs это не касается вовсе, потому что у неё ошибки не возникает. |
Как отвечать на собеседовании
Короткий ответ: звёздочка в объявлении собирает, звёздочка в вызове
раскладывает. Одна собирает лишние позиционные аргументы в кортеж args, две —
лишние именованные в словарь kwargs; в вызове одна раскладывает
последовательность в позиционные аргументы, две — словарь в именованные. И оба
объекта создаются даже когда передавать нечего: приходят () и {}, а не
None.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Первое — цена, и она у двух звёздочек разная: plain(*args) почти бесплатен, а
plain(**kwargs) дороже прямого вызова вчетверо и больше, устойчиво на всех
четырёх версиях.
Что отличает хороший ответ: назвать не цену, а то, что **kwargs, дописанный
«для гибкости», делает неизвестные имена именованных аргументов допустимыми для
этой функции. Вызов connect("db", timeuot=5) перестаёт быть TypeError и
молча уходит в options, а timeout остаётся равным 30. Хуже того, с 3.13 интерпретатор умеет подсказывать правильное имя —
и функция с **kwargs эту помощь выбрасывает, ведя себя на 3.14 так же, как
на 3.11.
Дальше спросят
Обёртка стоит с @functools.wraps, и inspect.signature показывает исходную сигнатуру. Значит, обёртка примет ровно то, что примет цель?
Нет. wraps возвращает сигнатуру инструментам, но не заставляет обёртку ей
следовать: обёртка пропускает и пять позиционных аргументов, и любое имя, ничего
не проверив. Падает уже вызов цели внутри — connect() takes from 1 to 3 positional arguments but 5 were given, — и в трассировке это видно по лишним
кадрам: их на один больше, чем слоёв.
Зачем в сигнатуре голая * и слеш /, если о порядке аргументов можно просто договориться?
Голая * запрещает передавать позиционно всё, что после неё: порядок
параметров перестаёт быть частью договора, и его можно менять, не ломая
вызывающих. Слеш делает обратное и нужен там, где функция принимает
**kwargs: пока параметр можно передать по имени, это имя занято, и ключ с
таким же названием в **kwargs не попадёт. Так объявлен сам dict —
($self, /, *args, **kwargs), — и поэтому dict(self="x") работает.
Частые заблуждения
*args — это список
Кортеж. type(args).__name__ даёт tuple на всех четырёх версиях. Менять его нельзя — и это не придирка: он собирается заново на каждом вызове, и изменяемость означала бы, что кто-то может сохранить ссылку и переписать чужие аргументы.
при вызове без аргументов args и kwargs равны None
Пустой кортеж и пустой словарь. Справочник языка: *identifier is initialized to a tuple… defaulting to the empty tuple
(инициализируется кортежем… по умолчанию пустым кортежем). Поэтому проверка if args: работает, а if args is not None: истинна всегда — и обе выглядят одинаково правдоподобно.
звёздочки — это просто запись, работы за ними нет
Работа есть, и разная у двух звёздочек. plain(*args) — ×1,1–1,3 от прямого вызова, plain(**kwargs) — ×4,0–5,2. Причина видна в байт-коде на всех четырёх версиях: кортеж уходит в вызов как есть, а перед вызовом со словарём выполняются BUILD_MAP и DICT_MERGE — то есть словарь копируется на каждом вызове.
**kwargs добавляет гибкости и ничего не отнимает
Отнимает отказ по неизвестному имени: с двумя звёздочками неизвестные имена становятся для функции допустимыми. connect("db", timeuot=5) без **kwargs — TypeError, с ним — молча принятый вызов, где timeout остался равен 30. И подсказка
(Вы имели в виду 'timeout'?), которой интерпретатор научился в 3.13, тоже не сработает: ошибки нет, привязывать подсказку не к чему.Did you mean 'timeout'?
functools.wraps чинит сигнатуру обёртки
Он возвращает сигнатуру ДЛЯ ИНСТРУМЕНТОВ, но не делает её правдой. inspect.signature после @wraps показывает (host, port=5432, timeout=30), а сама обёртка спокойно принимает пять позиционных аргументов и любое имя — исключение бросает внутренняя функция, и в трассировке появляется лишний кадр на каждый слой.
c и d в def f(a, *args, c, d=4) можно передать позиционно
Они только-именованные, и не из-за значений по умолчанию, а из-за позиции: всё после *args передаётся только по имени (PEP 3102). Видно в коде функции: co_kwonlyargcount равен двум. У c значения по умолчанию нет вовсе, и он всё равно только-именованный.
слеш в объявлении — украшение из документации
Без него имя параметра занято навсегда: ключ с таким же названием в **kwargs не попадёт, вызов даст got multiple values for argument. Именно поэтому dict объявлен со слешем: его сигнатура — ($self, /, *args, **kwargs), и dict(self="x") даёт {'self': 'x'}. Второй довод из PEP 570 — имена параметров перестают быть частью публичного договора и их можно менять.
аннотировать **kwargs нельзя
До 3.12 можно было, но одним типом на все значения — PEP 692 называет это «very limiting». С 3.12 есть Unpack[TypedDict], и он возвращает проверяющему имена и типы. Это единственный автоматический способ поймать timeuot=5 до запуска, когда **kwargs убрать нельзя.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
def connect(dsn, timeout=30, **options):
return timeout, options
print(connect("db", timeout=5))
print(connect("db", timeuot=5))Практика · оцените
Проверка знаний
Что вернёт probe() для def probe(*args, **kwargs): return args, kwargs?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Звёздочка делает противоположные вещи на двух сторонах вызова. В объявлении она собирает: одна — лишние позиционные аргументы в кортеж
args, две — лишние именованные в словарьkwargs. В вызове она раскладывает: одна — последовательность в отдельные позиционные аргументы, две — словарь в отдельные именованные. Собранные объекты функция получает даже когда передавать нечего: при вызове без аргументов внутрь приходят()и{}, а неNone. - Отсюда главное следствие: две звёздочки, дописанные в объявление «для гибкости», делают неизвестные имена именованных аргументов допустимыми для этой функции — их теперь есть куда положить. Вызов
connect("db", timeuot=5)перестаёт бытьTypeErrorи молча уходит вoptions, аtimeoutостаётся равным 30. Второе следствие — цена, и она у двух звёздочек разная:plain(*args)почти бесплатен (×1,1–1,3 от прямого вызова),plain(**kwargs)дороже вчетверо и больше (×4,0–5,2). Устойчиво на всех четырёх версиях; три слоя декораторов — ×8,5–11,9, то есть восемь девятых времени уходит не на работу, а на подход к ней. - Дальше — числа, версии и границы.
()и{}— это семантика, а не утверждение о выделении памяти: пустой кортеж в CPython один на всех, и сколько при этом выделяется — вопрос к замеру ниже, а не к языку. Допустимыми становятся ровно неизвестные имена, а остальные правила связывания никуда не деваются — прогонbench/args-kwargs/binding_rules.pyпроверяет каждое настоящим вызовом: strict(a=1, unknown=9) TypeError loose(a=1, unknown=9) a=1 b=2 kwargs={'unknown': 9} loose() TypeError ← обязательный параметр loose(1, a=2) TypeError ← дважды переданный positional_only_loose(a=1, b=2, c=3) TypeError ← имя вместо позиции- Последняя строка объясняет, почему
/в объявлении — не украшение. А самый неожиданный случай прогон печатает отдельно: у функцииdef f(a, /, b, *, c, **kwargs)вызовf(1, b=2, c=3, a=99)не ошибка —a=99уходит вkwargs, потому что позиционно-только параметр своё имя для ключевых аргументов не занимает. - Хуже того: в 3.13 интерпретатор научился подсказывать правильное имя — Did you mean 'timeout'?. Функция с
**kwargsэту помощь выбрасывает и на 3.14 ведёт себя так же, как на 3.11. /из PEP 570 нужен не для красоты: пока параметр можно передать по имени, это имя занято, и ключ с таким же названием в**kwargsне попадёт. Так объявлен самdict: его сигнатура —($self, /, *args, **kwargs).
На самом деле
- Кортеж.
type(args).__name__даётtupleна всех четырёх версиях. Менять его нельзя — и это не придирка: он собирается заново на каждом вызове, и изменяемость означала бы, что кто-то может сохранить ссылку и переписать чужие аргументы. - Пустой кортеж и пустой словарь. Справочник языка:
*identifierпо умолчанию пустым кортежем}>is initialized to a tuple… defaulting to the empty tuple. Поэтому проверкаif args:работает, аif args is not None:истинна всегда — и обе выглядят одинаково правдоподобно. - Работа есть, и разная у двух звёздочек.
plain(*args)— ×1,1–1,3 от прямого вызова,plain(**kwargs)— ×4,0–5,2. Причина видна в байт-коде на всех четырёх версиях: кортеж уходит в вызов как есть, а перед вызовом со словарём выполняютсяBUILD_MAPиDICT_MERGE— то есть словарь копируется на каждом вызове. - Отнимает отказ по неизвестному имени: с двумя звёздочками неизвестные имена становятся для функции допустимыми.
connect("db", timeuot=5)без**kwargs—TypeError, с ним — молча принятый вызов, гдеtimeoutостался равен 30. И подсказкаDid you mean 'timeout'?, которой интерпретатор научился в 3.13, тоже не сработает: ошибки нет, привязывать подсказку не к чему. - Он возвращает сигнатуру ДЛЯ ИНСТРУМЕНТОВ, но не делает её правдой.
inspect.signatureпосле@wrapsпоказывает(host, port=5432, timeout=30), а сама обёртка спокойно принимает пять позиционных аргументов и любое имя — исключение бросает внутренняя функция, и в трассировке появляется лишний кадр на каждый слой. - Они только-именованные, и не из-за значений по умолчанию, а из-за позиции: всё после
*argsпередаётся только по имени (PEP 3102). Видно в коде функции:co_kwonlyargcountравен двум. Уcзначения по умолчанию нет вовсе, и он всё равно только-именованный. - Без него имя параметра занято навсегда: ключ с таким же названием в
**kwargsне попадёт, вызов дастgot multiple values for argument. Именно поэтомуdictобъявлен со слешем: его сигнатура —($self, /, *args, **kwargs), иdict(self="x")даёт{'self': 'x'}. Второй довод из PEP 570 — имена параметров перестают быть частью публичного договора и их можно менять. - До 3.12 можно было, но одним типом на все значения — PEP 692 называет это «very limiting». С 3.12 есть
Unpack[TypedDict], и он возвращает проверяющему имена и типы. Это единственный автоматический способ пойматьtimeuot=5до запуска, когда**kwargsубрать нельзя.
По версиям
- 3.0
- PEP 3102 даёт голую
*и правило: всё после неё передаётся только по имени. Мотив записан прямо — функция с переменным числом аргументов, у которой есть ещё и «опции»; до этого их доставали из**kwargsруками. Отсюда жеco_kwonlyargcount— отдельное поле в коде функции.< - 3.8
- PEP 570 даёт
/. Нужен он не для симметрии: пока параметр передаётся по имени, это имя занято и ключ с таким же названием в**kwargsне попадёт. Проверено запуском:name_taken("a", name="b")—TypeError,name_free("a", name="b")— работает.< - 3.12
- PEP 692 разрешил
Unpack[TypedDict]для**kwargs: проверяющий снова видит имена и типы. До этого все значения обязаны были иметь один тип — that behaviour can be very limiting. Само имяUnpackпоявилось раньше, в 3.11 (PEP 646), поэтому такую аннотацию принимают и на более старом целевом уровне.< - 3.13
- Интерпретатор подсказывает правильное имя:
Did you mean 'timeout'?. Проверено на всех четырёх версиях — на 3.11 и 3.12 подсказки нет. Важная оговорка: функцию с**kwargsэто не касается вовсе, потому что у неё ошибки не возникает.<
Что разобрано
- База: звёздочка на двух сторонах вызова
- Механизм 1: что это за объекты
- Механизм 2: ошибка, которая не падает
- Механизм 3: `/` и голая `*` — зачем занимать имена
- Механизм 4: что обёртка делает с сигнатурой
- Механизм 5: типизация — чем вернуть стёртые имена
- Глубже: сколько стоят звёздочки
- История версий
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Справочник языка — вызовы и определения функцийОфициальная документация. Грамматика, из которой следует порядок частей объявления и правило «If the form `*identifier` is present, it is initialized to a tuple receiving any excess positional parameters, defaulting to the empty tuple» (Если присутствует форма `*identifier`, она инициализируется кортежем, принимающим все лишние позиционные параметры; по умолчанию — пустым кортежем). Там же про словарь: «If the form `**identifier` is present, it is initialized to a new ordered mapping receiving any excess keyword arguments, defaulting to a new empty mapping of the same type» (Если присутствует форма `**identifier`, она инициализируется новым упорядоченным отображением, принимающим все лишние именованные аргументы; по умолчанию — новым пустым отображением того же типа). Проверено запуском: при вызове без аргументов приходят `()` и `{}`, а не `None`.https://docs.python.org/3.14/reference/compound_stmts.html#function-definitions
- PEP 3102 — Keyword-Only ArgumentsPEP. Talin, Final, Python 3.0. Отсюда голая `*` в объявлении и правило, что всё после неё передаётся только по имени. Мотив назван прямо: «One can easily envision a function which takes a variable number of arguments, but also takes one or more 'options' in the form of keyword arguments» (Легко представить функцию, которая принимает переменное число аргументов и вдобавок одну или несколько „опций“ в виде именованных аргументов) — до этого такие «опции» приходилось доставать из `**kwargs` руками.https://peps.python.org/pep-3102/
- PEP 570 — Python Positional-Only ParametersPEP. Ларри Хастингс, Пабло Галиндо Сальгадо, Марио Корчеро, Эрик Вандер Виле; Final, Python 3.8. Две причины для `/`, и вторая проверяется запуском в уроке: «Without the ability to specify which parameters are positional-only, library authors must be careful when choosing appropriate parameter names» (Без возможности объявить параметры только-позиционными авторам библиотек приходится осторожничать с выбором имён параметров) и «A key scenario is when a function accepts any keyword argument but can also accepts a positional one» (Ключевой случай — когда функция принимает любой именованный аргумент, но при этом принимает и позиционный). Проверено запуском: `dict.__init__.__text_signature__` — `($self, /, *args, **kwargs)`, и слеш освобождает имя `self`, поэтому `dict(self="x")` даёт `{'self': 'x'}`.https://peps.python.org/pep-0570/
- PEP 692 — Using TypedDict for more precise **kwargs typingPEP. Франек Магера, Final, Python 3.12. Названо ограничение, которое до этого делало типизацию `**kwargs` почти бесполезной: «Currently **kwargs can be type hinted as long as all of the keyword arguments specified by them are of the same type. However, that behaviour can be very limiting» (Сейчас **kwargs можно аннотировать, только если все передаваемые через них именованные аргументы одного типа. Но такое поведение может сильно ограничивать). `Unpack[TypedDict]` — единственный способ вернуть проверяющему имена и типы, которые `**kwargs` стирает.https://peps.python.org/pep-0692/
- Что нового в Python 3.13 — улучшенные сообщения об ошибкахОфициальная документация. Изменение, прямо относящееся к теме урока: «The error message now tries to suggest the correct keyword argument when an incorrect keyword argument is passed to a function» (Сообщение об ошибке теперь пытается подсказать правильный именованный аргумент, если в функцию передали неверный), с примером `split() got an unexpected keyword argument 'max_split'. Did you mean 'maxsplit'?`. Проверено на всех четырёх версиях: на 3.11 и 3.12 подсказки нет, на 3.13 и 3.14 есть — и функция с `**kwargs` не получает её ни на одной.https://docs.python.org/3.14/whatsnew/3.13.html
- functools — wraps и update_wrapperОфициальная документация. Источник поведения, из-за которого сигнатура обёртки лжёт по-разному в зависимости от одной строки. Без `@wraps` обёртка показывает `(*args, **kwargs)`; с ним `inspect.signature` находит `__wrapped__` и показывает сигнатуру исходной функции — ту, которой обёртка не следует. Проверено обоими способами в
bench/args-kwargs/wrapper_signature.py.https://docs.python.org/3.14/library/functools.html#functools.wraps