ЗАМЕР
bench/introspection/closure_vars.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/closures-and-scope
- Как запустить
Ни в одном из трёх нет измерения времени: смотрим на значения, типы и признаки. Адреса объектов в выводе заменены на `0x...`, чтобы прогон был сравним между запусками. ## `inspect.unwrap` и `__signature__` Вывод **одинаков на всех четырёх версиях** (различается только строка `PY`):
Запись прогона
Замеры: интроспекция обёрток и замыканий
Три сюжета, которых нет в bench/decorators/ и bench/closures/: снятие
обёртки и подмена видимой сигнатуры, поломка признака «корутинная функция»
синхронной обёрткой, и разница между ячейкой замыкания и снимком значения.
| скрипт | что показывает | статьи |
|---|---|---|
unwrap_and_signature.py |
inspect.unwrap проходит цепочку __wrapped__ целиком; __signature__ перебивает и цепочку, и follow_wrapped=False |
декораторы |
async_decorator.py |
синхронная обёртка над async def даёт iscoroutinefunction() == False; три способа починки |
декораторы |
closure_vars.py |
inspect.getclosurevars; ячейка читается поздно, __defaults__ и partial — рано |
замыкания |
Запуск:
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/introspection/async_decorator.py
done
Ни в одном из трёх нет измерения времени: смотрим на значения, типы и признаки.
Адреса объектов в выводе заменены на 0x..., чтобы прогон был сравним между
запусками.
inspect.unwrap и __signature__
Вывод одинаков на всех четырёх версиях (различается только строка PY):
$ python3.13 unwrap_and_signature.py
PY 3.13.7
1) цепочка __wrapped__ сверху вниз: ['c', 'b', 'a', 'ОРИГИНАЛ']
три обёртки над одной функцией — и все зовутся 'target': ['target', 'target', 'target']
2) inspect.unwrap(three) is target: True
а three is target: False
один __wrapped__ снимает только слой: False | его layer: b
3) unwrap(..., stop=layer=='a') остановился на слое: a
4) цикл в __wrapped__ -> ValueError: wrapper loop when unwrapping <function loop_a at 0x...>
5) видимые сигнатуры одной и той же обёртки:
signature(three) (x: int, y: str = 's') -> bool
signature(three, follow_wrapped=False) (*a, **kw) -> bool
реальные параметры обёртки ('a', 'kw')
6) обёртка с __signature__:
signature(lying) (count: int, *, label: str = '?') -> None
signature(lying, follow_wrapped=False) (count: int, *, label: str = '?') -> None
__wrapped__ по-прежнему указывает на target: True
но unwrap всё равно доходит до target: True
7) __signature__ стоит на ВНУТРЕННЕЙ обёртке:
signature(outer_over_inner) (count: int, *, label: str = '?') -> None
(не (x: int, y: str = 's') -> bool — раскрутка встала на слое с __signature__)
8) что увидит генератор документации:
str(inspect.signature(lying)) -> "(count: int, *, label: str = '?') -> None"
вызов lying(1) при этом уйдёт в target и сработает: True
bind(count=1, label='x') по ПОДДЕЛЬНОЙ сигнатуре прошёл
а реальный вызов lying(count=1, label='x') -> TypeError: target() got an unexpected keyword argument 'count'
Три строки, ради которых написан файл.
Пункт 6: follow_wrapped=False — не способ добраться до правды. Он отключает
проход по __wrapped__, но __signature__ стоит на самой обёртке, и
поэтому побеждает в обоих случаях. Единственный, кто по-прежнему честен, —
__code__ (пункт 5, ('a', 'kw')).
Пункт 7: __signature__ на любом слое цепочки останавливает раскрутку внутри
signature, но не внутри unwrap. Это два разных обхода одной цепочки.
Пункт 8: расхождение между заявленной и настоящей сигнатурой не ловится ничем.
Signature.bind проверяет по подделке и пропускает вызов, который упадёт.
Первоисточники, Doc/library/inspect.rst (тег v3.14.5):
строки 1289–1303,
unwrap:Get the object wrapped by func. It follows the chain of
__wrapped__attributes returning the last object in the chain. […] For example,signatureuses this to stop unwrapping if any object in the chain has a__signature__attribute defined.ValueErroris raised if a cycle is encountered.Перевод: «Получить объект, обёрнутый func. Функция идёт по цепочке атрибутов
__wrapped__и возвращает последний объект цепочки. […] Например,signatureиспользует это, чтобы остановить раскрутку, если у какого-нибудь объекта в цепочке определён атрибут__signature__.ValueErrorвозбуждается при обнаружении цикла.»строки 803–808,
impl-detailвнутри описанияsignature:If the passed object has a
__signature__attribute, we may use it to create the signature. The exact semantics are an implementation detail and are subject to unannounced changes. Consult the source code for current semantics.Перевод: «Если у переданного объекта есть атрибут
__signature__, мы можем использовать его для построения сигнатуры. Точная семантика — деталь реализации и может измениться без объявления. За текущей семантикой обращайтесь к исходному коду.»Формулировка проверена на всех четырёх версиях документации (теги
v3.11.15,v3.12.3,v3.13.7,v3.14.5) — она дословно одна и та же. То есть «деталь реализации» здесь не означает «менялось»: не менялось.
Асинхронный декоратор
| 3.11.15 | 3.12.3 | 3.13.7 | 3.14.7 | |
|---|---|---|---|---|
iscoroutinefunction(sync_wrapper) |
False | False | False | False |
inspect.markcoroutinefunction существует |
нет | есть | есть | есть |
asyncio.iscoroutinefunction даёт DeprecationWarning |
нет | нет | нет | да |
$ python3.11 async_decorator.py
PY 3.11.15
1) что скопировал wraps и что не скопировал:
__name__ : job
__doc__ : 'Настоящая корутинная функция.'
__wrapped__ is job: True
iscoroutinefunction(job) : True
iscoroutinefunction(broken): False
2) признак — флаг кода, а не атрибут:
job.__code__.co_flags & CO_COROUTINE : True
broken.__code__.co_flags & CO_COROUTINE: False
asyncio.iscoroutinefunction(broken): False
3) диспетчер, который смотрит на iscoroutinefunction:
dispatch(job, 21) -> 42
dispatch(broken, 21)-> coroutine <coroutine object job at 0x...>
вместо числа наружу уехал объект корутины: он не выполнен,
и при сборке мусора даст RuntimeWarning 'was never awaited'
4) починка A — обёртка сама async def:
iscoroutinefunction(fixed_a): True
результат: 42
5) починка B — inspect.markcoroutinefunction:
доступна: False — на этой версии markcoroutinefunction нет
единственный переносимый способ — сделать обёртку async def
6) декоратор, выбирающий форму обёртки по цели:
iscoroutinefunction(smart_deco(job)) : True
iscoroutinefunction(smart_deco(plain)): False
smart_deco(plain)(1) = 2 | asyncio.run(smart_deco(job)(21)) = 42
7) iscoroutinefunction по __wrapped__ НЕ идёт:
iscoroutinefunction(broken) : False
iscoroutinefunction(inspect.unwrap(broken)): True
то есть проверять надо оригинал, а чинить — обёртку
На 3.12.3 и 3.13.7 пункт 5 выглядит так:
5) починка B — inspect.markcoroutinefunction:
доступна: True
iscoroutinefunction(marked): True
а флаг кода по-прежнему НЕ корутинный: False
маркер — обычный атрибут: True
asyncio.iscoroutinefunction(marked): True
На 3.14.7 к обеим строкам с asyncio.iscoroutinefunction добавляется:
[DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for removal in Python 3.16; use inspect.iscoroutinefunction() instead]
Что тут важно. Пункт 2: признак — это флаг объекта кода CO_COROUTINE
(0x0080), а не атрибут функции; functools.wraps копирует атрибуты и до
флага не дотягивается по определению. Пункт 5: markcoroutinefunction чинит
ответ iscoroutinefunction, не трогая флаг, — он ставит обычный атрибут
_is_coroutine_marker. Пункт 7: iscoroutinefunction не идёт по
__wrapped__, поэтому подсказка «оберни в wraps и всё наладится» не работает.
Первоисточник — Doc/library/inspect.rst, строки 438–472 (тег v3.14.5):
Return
Trueif the object is a coroutine function (a function defined with anasync defsyntax), afunctools.partialwrapping a coroutine function, or a sync function marked withmarkcoroutinefunction. […].. versionchanged:: 3.12— Sync functions marked withmarkcoroutinefunctionnow returnTrue.
Перевод: «Возвращает True, если объект — корутинная функция (функция,
определённая синтаксисом async def), functools.partial вокруг корутинной
функции или синхронная функция, помеченная markcoroutinefunction. […]
Изменено в версии 3.12: синхронные функции, помеченные
markcoroutinefunction, теперь дают True.»
И там же, строки 460–473:
markcoroutinefunction(func)— Decorator to mark a callable as a coroutine function if it would not otherwise be detected byiscoroutinefunction. […] When possible, using anasync deffunction is preferred... versionadded:: 3.12
Перевод: «Декоратор, помечающий вызываемый объект как корутинную функцию,
если иначе iscoroutinefunction её не распознаёт. […] Когда возможно,
предпочтительна функция async def. Добавлено в версии 3.12.»
Ячейка против снимка
Вывод closure_vars.py одинаков на всех четырёх версиях:
$ python3.13 closure_vars.py
PY 3.13.7
1) getclosurevars(by_closure):
nonlocals: {'n': 2}
globals : {'GLOBAL_RATE': 10}
builtins : {'len': <built-in function len>}
unbound : set()
2) getclosurevars(by_default) — n НЕ в nonlocals, он параметр:
nonlocals: {}
globals : {'GLOBAL_RATE': 10}
а значение снимка лежит здесь: (2,)
3) getclosurevars(partial) -> TypeError: functools.partial(<function make_all_three.<locals>.body at 0x...>, 2) is not a Python function
значение снимка у partial: (2,) | func: body
getclosurevars(partial.func): {}
4) unbound у функции с несуществующим именем: {'NOT_DEFINED_ANYWHERE'}
5) до переприсваивания: первое | первое | первое
после rebind('второе'): второе | первое | первое
замыкание видит новое значение, снимки — старое
6) хранилища:
closure: __closure__ = (<cell at 0x...: str object at 0x...>,) -> cell_contents = ['второе']
default: __defaults__ = ('первое',) | __closure__ = None
partial: .args = ('первое',) | .keywords = {}
7) запись в cell_contents снаружи: третье
запись в __defaults__ снаружи: четвёртое
partial неизменяем — нужен новый объект: пятое
8) три способа в цикле:
замыкание по i : [2, 2, 2]
аргумент по умолч. : [0, 1, 2]
functools.partial : [0, 1, 2]
Пункты 1–3 — про интроспекцию: getclosurevars показывает только ячейки,
глобальные и встроенные имена. Снимок в аргументе по умолчанию для него не
существует (пункт 2: nonlocals пуст), а functools.partial он вообще
отказывается принимать — TypeError, потому что это не питоновская функция.
Пункты 5–8 — про поведение: три способа «запомнить значение» дают одинаковый
результат в цикле (пункт 8) и разный при переприсваивании (пункт 5). Ровно та
же граница видна и в хранилищах (пункт 6): __closure__ — кортеж ячеек,
__defaults__ — кортеж значений, partial.args — кортеж значений.
Первоисточник — Doc/library/inspect.rst, строки 1273–1287 (тег v3.14.5):
Get the mapping of external name references in a Python function or method func to their current values. A named tuple
ClosureVars(nonlocals, globals, builtins, unbound)is returned. nonlocals maps referenced names to lexical closure variables, globals to the function's module globals and builtins to the builtins visible from the function body. unbound is the set of names referenced in the function that could not be resolved at all given the current module globals and builtins.TypeErroris raised if func is not a Python function or method.
Перевод: «Получить отображение внешних имён, на которые ссылается питоновская
функция или метод func, в их текущие значения. Возвращается именованный
кортеж ClosureVars(nonlocals, globals, builtins, unbound). nonlocals
сопоставляет упомянутые имена переменным лексического замыкания, globals —
глобальным именам модуля функции, builtins — встроенным именам, видимым из
тела функции. unbound — множество имён, упомянутых в функции, которые вообще
не удалось разрешить при текущих глобальных и встроенных именах модуля.
TypeError возбуждается, если func не является питоновской функцией или
методом.»
Слово «current values» в первом предложении — не оборот речи: пункт 7 меняет
cell_contents снаружи, и следующий вызов getclosurevars показал бы уже
новое значение. Снимок так не умеет.
Оговорка про версию
3.14 здесь — это 3.14.7. Единственное, что взято именно с неё, —
текст DeprecationWarning у asyncio.iscoroutinefunction. Все остальные
строки на 3.14 совпадают с 3.13.7.
Тег исходников CPython для цитат из Doc/ — v3.14.5; формулировка про
__signature__ дополнительно сверена с тегами v3.11.15, v3.12.3 и
v3.13.7.
Скрипт
129 строк"""inspect.getclosurevars и снимок значения: аргумент по умолчанию против functools.partial.
Один сюжет: у «замыкания» и у «снимка» разные механизмы хранения, и они
по-разному видны интроспекции. Замыкание держит ЯЧЕЙКУ (значение читается в
момент вызова), аргумент по умолчанию и partial держат ЗНАЧЕНИЕ (прочитано в
момент создания). getclosurevars показывает только первое.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import functools
import inspect
import re
import sys
def noaddr(x):
"""Адреса объектов заменены, чтобы вывод был воспроизводим между запусками."""
return re.sub(r"0x[0-9a-f]+", "0x...", str(x))
print("PY", sys.version.split()[0])
print()
GLOBAL_RATE = 10
def make_all_three(n):
"""Три способа «запомнить» n."""
def by_closure():
return n * GLOBAL_RATE + len("x")
def by_default(n=n):
return n * GLOBAL_RATE + len("x")
def body(n):
return n * GLOBAL_RATE + len("x")
by_partial = functools.partial(body, n)
return by_closure, by_default, by_partial, body
closure_fn, default_fn, partial_fn, body_fn = make_all_three(2)
# --- 1. Что показывает getclosurevars --------------------------------------
cv = inspect.getclosurevars(closure_fn)
print("1) getclosurevars(by_closure):")
print(" nonlocals:", cv.nonlocals)
print(" globals :", cv.globals)
print(" builtins :", cv.builtins)
print(" unbound :", cv.unbound)
print()
cv2 = inspect.getclosurevars(default_fn)
print("2) getclosurevars(by_default) — n НЕ в nonlocals, он параметр:")
print(" nonlocals:", cv2.nonlocals)
print(" globals :", cv2.globals)
print(" а значение снимка лежит здесь:", default_fn.__defaults__)
print()
# --- 3. partial getclosurevars не принимает --------------------------------
try:
inspect.getclosurevars(partial_fn)
print("3) getclosurevars(partial) — принял")
except TypeError as e:
print("3) getclosurevars(partial) ->", type(e).__name__ + ":", noaddr(e))
print(" значение снимка у partial:", partial_fn.args, "| func:", partial_fn.func.__name__)
print(" getclosurevars(partial.func):", inspect.getclosurevars(partial_fn.func).nonlocals)
print()
# --- 4. unbound: имя, которое не нашлось -----------------------------------
def uses_missing():
return NOT_DEFINED_ANYWHERE + 1 # noqa: F821
print("4) unbound у функции с несуществующим именем:",
inspect.getclosurevars(uses_missing).unbound)
print()
# --- 5. Главная разница: ячейка читается ПОЗДНО, снимок — РАНО -------------
def make_reader():
value = "первое"
def by_closure():
return value
def by_default(v=value):
return v
by_partial = functools.partial(lambda v: v, value)
def rebind(new):
nonlocal value
value = new
return by_closure, by_default, by_partial, rebind
c, d, p, rebind = make_reader()
print("5) до переприсваивания:", c(), "|", d(), "|", p())
rebind("второе")
print(" после rebind('второе'):", c(), "|", d(), "|", p())
print(" замыкание видит новое значение, снимки — старое")
print()
# --- 6. Где физически лежит каждое из трёх ---------------------------------
print("6) хранилища:")
print(" closure: __closure__ =", noaddr(c.__closure__),
"-> cell_contents =", [cell.cell_contents for cell in c.__closure__])
print(" default: __defaults__ =", d.__defaults__, "| __closure__ =", d.__closure__)
print(" partial: .args =", p.args, "| .keywords =", p.keywords)
print()
# --- 7. Ячейку можно переписать снаружи, снимок — тоже, но иначе -----------
c.__closure__[0].cell_contents = "третье"
print("7) запись в cell_contents снаружи:", c())
d.__defaults__ = ("четвёртое",)
print(" запись в __defaults__ снаружи:", d())
p2 = functools.partial(p.func, "пятое")
print(" partial неизменяем — нужен новый объект:", p2())
print()
# --- 8. Классическая ловушка цикла и её починка снимком ---------------------
late = [lambda: i for i in range(3)]
early = [lambda i=i: i for i in range(3)]
parts = [functools.partial(lambda i: i, i) for i in range(3)]
print("8) три способа в цикле:")
print(" замыкание по i :", [f() for f in late])
print(" аргумент по умолч. :", [f() for f in early])
print(" functools.partial :", [f() for f in parts])