ЗАМЕР
bench/args-kwargs/binding_rules.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/args-kwargs
- Как запустить
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v cost.py; done
Запись прогона
Замеры для урока «*args и **kwargs»
| скрипт | что показывает |
|---|---|
identity.py |
*args — кортеж, **kwargs — словарь, и оба создаются даже при пустом вызове; что о звёздочках знает код функции (co_flags, co_kwonlyargcount, co_posonlyargcount) |
swallowed_typo.py |
**kwargs, добавленный «для гибкости», превращает опечатку в имени аргумента из TypeError в молчаливо принятое значение по умолчанию |
cost.py |
цена именованной передачи, распаковки кортежа, распаковки словаря, приёма звёздочками и обёрток-декораторов |
wrapper_signature.py |
что обёртка делает с подписью функции и куда уезжает ошибка |
Запускать на всех версиях, которые есть:
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v cost.py; done
Практика урока
practice.py — источник ответов двух практических задач урока, а runs/practice.txt —
дословная запись его прогона. Ответ задачи не сочиняется: сборка сверяет
заявленное с этой записью (scripts/validate-practice.mjs) и не проходит,
если они разошлись.
Прогон снят 30.08.2026 на CPython 3.13.7 (Clang 20.1.4). Абсолютные числа — этой машины; переносится кратность, и задача «во сколько раз» стоит именно на ней.
Кратность устойчива только потому, что формы меряются ВПЕРЕМЕЖКУ: в каждом круге меряются все, минимум для каждой берётся по кругам. Пока замеры шли подряд, просадка машины в окне одной формы целиком доставалась ей, и отношение гуляло в полтора раза от запуска к запуску (измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.
Что здесь сравнивать можно, а что нельзя
Общее правило замеров: время между версиями не сравнивается вообще.
Сравнивается только то, что измерено внутри одного запуска одного
интерпретатора. Дело не только в компиляторе: 3.11 и 3.12 собраны GCC 13.3.0,
3.13.7 и 3.14.7 — Clang 20.1.4, но и эти две сборки различаются между собой,
причём ровно тем флагом (--with-tail-call-interp), которому «Что нового в
3.14» приписывает «a geometric mean of 3-5% faster».
В cost.py все сравнения делаются внутри одного запуска одного
интерпретатора: семь форм вызова одной и той же функции меряются подряд
одним процессом. Такое сравнение честно всегда. Числа 3.13.7 и 3.14.7
приводятся рядом как два независимых результата, а не как сравнение.
identity.py, swallowed_typo.py и wrapper_signature.py от тулчейна не
зависят вовсе: они смотрят на типы, флаги, подписи и текст исключений.
Разброс между запусками
cost.py — лучшее из семи прогонов. Три повторных запуска подряд на 3.13.7
дали 24,3 / 24,2 / 24,2 нс на позиционном вызове и 211 / 207 / 206 нс на трёх
обёртках, то есть разброс около ±2 %. Все кратности, названные в уроке
(×1,4 на именованной передаче, ×4 на распаковке словаря, ×8,5 на трёх
обёртках), в этот разброс не укладываются — они настоящие.
Чего в cost.py НЕТ и почему
Нет строки plain(*ARGS, **KWARGS). Она передала бы два позиционных и
два именованных аргумента в функцию с двумя параметрами, то есть упала бы с
got multiple values for argument 'a'. Обе распаковки сразу происходят в
строках про обёртки — там они и измерены, на вызове, который действительно
работает.
Нет сравнения с functools.partial и с вызовом метода: и то и другое —
другой механизм передачи, и их место в отдельном замере, а не в строке,
которую прочтут как «звёздочки против непонятно чего».
Замеченное попутно: подсказка имени появилась в 3.13
connect("db", timeuot=5) у функции без **kwargs:
3.11.15 -> connect() got an unexpected keyword argument 'timeuot'
3.12.3 -> connect() got an unexpected keyword argument 'timeuot'
3.13.7 -> connect() got an unexpected keyword argument 'timeuot'. Did you mean 'timeout'?
3.14.7 -> то же, что 3.13.7
Это прямо относится к уроку: интерпретатор научился ловить ровно эту опечатку
лучше, а функция, объявленная с **kwargs, эту помощь выбрасывает — и на
3.14 ведёт себя так же, как на 3.11. Проверено swallowed_typo.py и
wrapper_signature.py.
Скрипт
93 строк"""Что `**kwargs` отключает, а что остаётся включённым.
ЗАЧЕМ ЭТОТ ФАЙЛ. Урок говорил: «`**kwargs` отключает проверку имён». Аудит
сузил: отключается ровно одно — отказ от НЕИЗВЕСТНЫХ ключевых имён, потому что
их теперь есть куда положить. Всё остальное связывание аргументов работает как
работало: обязательный параметр обязан быть передан, дважды переданный
параметр — ошибка, позиционно-только остаётся позиционно-только.
Разница практическая, а не терминологическая: «отключает проверку» звучит как
«обёртка с `**kwargs` проглотит любой вызов», и человек, который так понял,
ищет причину падения не там.
КАЖДАЯ СТРОКА — НАСТОЯЩИЙ ВЫЗОВ. Печатается результат или ТИП ошибки: тексты
сообщений между версиями меняются, поведение — нет.
ЗАПУСК: python3.13 bench/args-kwargs/binding_rules.py
Вывод по версиям: runs/binding_rules-3.11.txt и соседние.
"""
import inspect
import sys
def show(title: str) -> None:
print()
print(title)
print("-" * len(title))
def call(label: str, fn, *args, **kwargs) -> None:
try:
print(f" {label:<46} {fn(*args, **kwargs)}")
except TypeError as exc:
print(f" {label:<46} {type(exc).__name__}")
def strict(a, b=2):
return f"a={a} b={b}"
def loose(a, b=2, **kwargs):
return f"a={a} b={b} kwargs={kwargs}"
def positional_only(a, /, b, *, c):
return f"a={a} b={b} c={c}"
def positional_only_loose(a, /, b, *, c, **kwargs):
return f"a={a} b={b} c={c} kwargs={kwargs}"
def main() -> None:
print(f"Python {sys.version.split()[0]} ({sys.implementation.name})")
show("1. То единственное, что меняет **kwargs: неизвестное имя")
call("strict(a=1, unknown=9)", strict, a=1, unknown=9)
call("loose(a=1, unknown=9)", loose, a=1, unknown=9)
show("2. Обязательный параметр остаётся обязательным")
call("strict()", strict)
call("loose()", loose)
call("loose(unknown=9)", loose, unknown=9)
show("3. Дважды переданный параметр остаётся ошибкой")
call("strict(1, a=2)", strict, 1, a=2)
call("loose(1, a=2)", loose, 1, a=2)
show("4. Позиционно-только и ключевое-только не сдвигаются")
call("positional_only(a=1, b=2, c=3)", positional_only, a=1, b=2, c=3)
call("positional_only_loose(a=1, b=2, c=3)", positional_only_loose, a=1, b=2, c=3)
call("positional_only_loose(1, 2, 3)", positional_only_loose, 1, 2, 3)
call("positional_only_loose(1, 2, c=3)", positional_only_loose, 1, 2, c=3)
show("5. Куда попало имя, совпавшее с позиционно-только параметром")
call("positional_only_loose(1, b=2, c=3, a=99)", positional_only_loose, 1, b=2, c=3, a=99)
show("6. То же самое до вызова: inspect.Signature.bind")
signature = inspect.signature(loose)
for label, kwargs in [
("bind(a=1, unknown=9)", {"a": 1, "unknown": 9}),
("bind(unknown=9)", {"unknown": 9}),
]:
try:
bound = signature.bind(**kwargs)
print(f" {label:<46} {bound.arguments}")
except TypeError as exc:
print(f" {label:<46} {type(exc).__name__}")
if __name__ == "__main__":
main()