ЗАМЕР
bench/args-kwargs/wrapper_signature.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.
Скрипт
76 строк"""Что обёртка на `*args, **kwargs` делает с подписью функции и с ошибкой.
Обёртка принимает что угодно — в этом её смысл. Отсюда два следствия, о
которых обычно не думают, и оба проверяются запуском:
1. ЧТО ВИДЯТ ИНСТРУМЕНТЫ. Без `functools.wraps` подпись обёрнутой функции
превращается в `(*args, **kwargs)`, и подсказки редактора, справка и
проверка типов теряют имена параметров. С `wraps` подпись возвращается —
но она уже не описывает то, что обёртка на самом деле готова принять.
2. ГДЕ ВОЗНИКАЕТ ОШИБКА. Неверное имя аргумента обёртка пропускает молча
(ей всё годится) — исключение бросает уже внутренняя функция. В
трассировке появляется лишний кадр, а на нескольких обёртках — несколько.
От тулчейна ничего не зависит: смотрим подписи, имена и текст исключения.
"""
import functools
import inspect
import sys
import traceback
def connect(host, port=5432, timeout=30):
return host, port, timeout
def bare(fn):
def inner(*args, **kwargs):
return fn(*args, **kwargs)
return inner
def wrapped(fn):
@functools.wraps(fn)
def inner(*args, **kwargs):
return fn(*args, **kwargs)
return inner
no_wraps = bare(connect)
with_wraps = wrapped(connect)
three_deep = wrapped(wrapped(wrapped(connect)))
print("PY", sys.version.split()[0])
print(" что показывает inspect.signature:")
for label, fn in (("исходная", connect), ("обёртка без wraps", no_wraps), ("обёртка с wraps", with_wraps)):
print(f" {label:<20} {fn.__name__:<10} {inspect.signature(fn)}")
print(" но принимает обёртка что угодно:")
print(f" with_wraps(1, 2, 3, 4, 5, что_угодно=6) -> ", end="")
try:
with_wraps(1, 2, 3, 4, 5, что_угодно=6)
print("прошло")
except TypeError as exc:
print(f"TypeError от ВНУТРЕННЕЙ функции: {exc}")
print(" откуда прилетает ошибка при опечатке в имени:")
for label, fn in (("напрямую", connect), ("через одну обёртку", with_wraps), ("через три", three_deep)):
try:
fn("db", timeuot=5)
except TypeError as exc:
frames = traceback.extract_tb(exc.__traceback__)
# Кадр 0 — эта строка; остальные — обёртки и сама функция.
print(f" {label:<20} кадров в трассировке: {len(frames)}")
print(f" {'':<20} {exc}")
print(" подсказка про правильное имя (появилась в 3.13):")
try:
connect("db", timeuot=5)
except TypeError as exc:
has_hint = "Did you mean" in str(exc)
print(f" интерпретатор подсказывает имя: {has_hint}")