Недавно мне пришлось работать над проектом, который включает в себя отслеживание стека вызовов Python и получение вызывающих объектов следующего уровня для определенных функций, и мне нужен был путь к файлу нужного вызывающего объекта. Это используется для идентификации владельца вызывающей функции на основе файла CODEOWNERS на GitHub.
Решение казалось достаточно простым: либо использовать inspect.stack()
для получения всего стека, либо использовать sys._getframe().f_back
для получения кадров вызывающей стороны один за другим.
После захвата нужного объекта кадра f
путь к файлу вызывающей функции будет f.f_code.co_filename
.
Это хорошо работает, если у вас есть следующие простые вызовы функций.
Модуль tracing
:
# tracing.py import inspect def print_stack(): stack = inspect.stack() print(stack)
Модуль callee
:
# callee.py import tracing def func1(): tracing.print_stack()
Модуль callers
:
# callers.py from callee import * def caller(): func1()
И модуль test
:
# test.py from callers import * caller()
Предполагая, что мы хотим захватить прямого вызывающего абонента callee.func1()
, callers.py
, стек будет выглядеть следующим образом.
In [2]: import test [ ( <frame object at 0x7f9cf10240b0>, 'tracing.py', 6, 'print_stack', [' stack = inspect.stack()\n'], 0), ( <frame object at 0x10d2ae420>, 'callee.py', 7, 'func1', [' tracing.print_stack()\n'], 0), ( <frame object at 0x7f9ca65987c0>, 'callers.py', 7, 'caller', [' func1()\n'], 0), ( <frame object at 0x10d2ae608>, 'test.py', 4, '<module>', ['caller()\n'], 0), ( <frame object at 0x10d2ae7f0>, '<ipython-input-2-a0954f22e2a4>', 1, '<module>', [u'import test\n'], 0), ...
Из стека вызовов вызов callee.func1()
происходит в stack[2]
, а имя файла callers.py
, вот и все.
Однако наивная мысль, что я закончил проект, испарилась, как только я запустил то же самое на украшенной функции. См. пример ниже с недавно добавленным модулем decorators
.
Модуль decorators
:
# decorators.py from functools import wraps from callee import * def decor(inner_fn): @wraps(inner_fn) def wrapper(*args, **kwargs): func1() return inner_fn(*args, **kwargs) return wrapper
Модуль tracing
:
# tracing.py import inspect def print_stack(): stack = inspect.stack() print(stack)
Модуль callee
:
# callee.py import tracing def func1(): tracing.print_stack()
Модуль callers
с дополнительным декоратором:
# callers.py from decorators import * @decorators.decor def caller(): pass
И модуль test
:
# test.py from callers import * caller()
Здесь мы помещаем callee.func1()
внутрь функции декоратора decorators.decor()
. Я предполагаю, что стек будет выглядеть так:
tracing.print_stack callee.func1 decorators.decor callers.caller <- this is the one we want test
Запустив эту украшенную версию, мы получим:
In [1]: import test [ ( <frame object at 0x10a6598e8>, 'tracing.py', 6, 'print_stack', [' stack = inspect.stack()\n'], 0), ( <frame object at 0x10a5403a0>, 'callee.py', 8, 'func1', [' tracing.print_stack()\n'], 0), ( <frame object at 0x10a65e3f0>, 'decorators.py', 37, 'wrapper', [' func1()\n'], 0), ( <frame object at 0x10a5f6a70>, 'test.py', 4, '<module>', ['caller()\n'], # <- we see caller() 0), ( <frame object at 0x10a5f68c0>, '<ipython-input-1-a0954f22e2a4>', 1, '<module>', [u'import test\n'], 0),
Подожди, а где callers
? caller()
присутствует в кадре test.py
, и он вызывает callers.caller()
.
Здесь произошло то, что когда callers.caller()
украшено @decorators.decor
, вызов его эквивалентен decorator.decor(callers.caller)()
@decorator.decor def caller(): pass # and this call caller() # is equivalent to decorator.decor(caller)() # and this is kind of equivalent to calling the closure wrapper()
Из-за этой семантики callers.caller() никогда не вызывается, и поэтому у него никогда не было собственного фрейма.
Это создало проблему, заключающуюся в том, что мы не можем напрямую получить путь к файлу, применяя f.f_code.co_filename
для объекта фрейма f
.
Чтобы найти callers.caller(), мы исследуем локальное пространство имен фрейма test.py по индексу 2,
( <frame object at 0x10a65e3f0>, 'decorators.py', 37, 'wrapper', [' func1()\n'], 0),
с использованием
def print_stack(): stack = inspect.stack() print(stack) f = stack[2][0] print(f.f_locals)
Результат:
{ 'args': (), 'inner_fn': <function caller at 0x10f6629b0>, 'kwargs': {} }
Это локальные или свободные переменные функции wrapper()
, и мы знаем, что «inner_fn» — это обертываемая функция. Давайте проверим это с помощью
def print_stack(): stack = inspect.stack() print(stack) f = stack[2][0] print(f.f_locals['inner_fn'].__name__) print(f.f_locals['inner_fn'].__code__)
Мы получили
caller <code object caller at 0x10d240f30, file "callers.py", line 12>
Прохладный! Теперь мы снова на ходу. Конечно, невозможно всегда знать имя переменной, используемой для обернутой функции. Тем не менее разумно предположить, что переменная, вероятно, будет функцией, методом или классом, в котором определен метод __call__().
Чтобы программно получить вызывающую функцию и путь к ее файлу. Мы можем сделать это
def print_stack(): stack = inspect.stack() print(stack) f = stack[2][0] for k,v in f.f_locals.items(): if callable(v) and k in f.f_code.co_freevars: print(k, v.__name__, v.__code__)
Это проверяет, является ли локальная переменная объектом callable
, и мы также можем убедиться, что вызываемая переменная является свободной переменной. По какой-то причине f.f_code.__closure__ недоступен, несмотря на то, что у нас есть непустой f.f_code.co_freevars.
Выход
('inner_fn', 'caller', <code object caller at 0x101aadf30, file "callers.py", line 12>)
Значит ли это, что мы закончили?
Ну, вот еще один поворот сюжета, наш алгоритм дает сбой, если у нас есть более одного декоратора.
Добавляем несколько новых декораторов
# callers.py from decorators import * @decorators.decor2 @decorators.decor1 def caller(): pass
И декораторы определены здесь
# decorators.py from functools import wraps from callee import * def decor1(inner_fn1): @wraps(inner_fn1) def wrapper1(*args, **kwargs): func1() return inner_fn1(*args, **kwargs) return wrapper1 def decor2(inner_fn2): @wraps(inner_fn2) def wrapper2(*args, **kwargs): func1() return inner_fn2(*args, **kwargs) return wrapper2
Имена функций и переменных были изменены, чтобы мы могли четко идентифицировать их в стеке.
Запустив тот же test.py, мы получим 2 трассировки стека
# Stack 1 [ ( <frame object at 0x1070e0050>, 'tracing.py', 6, 'print_stack', [' stack = inspect.stack()\n'], 0), ( <frame object at 0x106fb43a0>, 'callee.py', 11, 'func2', [' tracing.print_stack()\n'], 0), ( <frame object at 0x1070d95c0>, 'decorators.py', 44, 'wrapper2', [' func2()\n'], 0), ( <frame object at 0x107071a70>, 'test.py', 4, '<module>', ['caller()\n'], 0), ( <frame object at 0x1070718c0>, '<ipython-input-1-a0954f22e2a4>', 1, '<module>', [u'import test\n'], 0), ... # Stack 2 [ ( <frame object at 0x1070e0410>, 'tracing.py', 6, 'print_stack', [' stack = inspect.stack()\n'], 0), ( <frame object at 0x7fd73ac8f8b0>, 'callee.py', 8, 'func1', [' tracing.print_stack()\n'], 0), ( <frame object at 0x7fd73ac8f5d0>, 'decorators.py', 37, 'wrapper1', [' func1()\n'], 0), ( <frame object at 0x1070d95c0>, 'decorators.py', 45, 'wrapper2', [' return inner_fn2(*args, **kwargs)\n'], 0), ( <frame object at 0x107071a70>, 'test.py', 4, '<module>', ['caller()\n'], 0), ( <frame object at 0x1070718c0>, '<ipython-input-1-a0954f22e2a4>', 1, '<module>', [u'import test\n'], 0), ...
Совершенно очевидно, что Stack 1
происходит от decorators.decor2
, а Stack 2
происходит от decorators.decor1
. Поскольку decorators.decor1
является внутренним декоратором, мы видим и decor2
, и decor1
, а стек печатается func1()
.
Давайте посмотрим, сможем ли мы получить вызывающую программу и ее путь к файлу из локального пространства имен f.f_locals, как мы это делали раньше.
Чтобы создать больше напряжения, мы сначала посмотрим на Stack 2
.
Похоже, кадр 2 и кадр 3 имеют значение.
print("2==================") f = stack[2][0] for k,v in f.f_locals.items(): if callable(v) and k in f.f_code.co_freevars: print(k, v.__name__, v.__code__) print("==================") print("3==================") f = stack[3][0] for k,v in f.f_locals.items(): if callable(v) and k in f.f_code.co_freevars: print(k, v.__name__, v.__code__) print("==================")
Это дает нам
2================== ('inner_fn1', 'caller', <code object caller at 0x10702af30, file "callers.py", line 11>) =================== 3================== ('inner_fn2', 'caller', <code object wrapper1 at 0x1070db630, file "decorators.py", line 35>) ==================
Похоже, все имеет смысл. wrapper2
внутри decor2
, оборачивает wrapper1
внутри decor1
. wrapper1
возвращает inner_fn1
, а wrapper2
возвращает inner_fn2
, и оба вызываемых объекта фактически являются функцией caller
.
Хорошо, давайте посмотрим на Stack 1
, который является трассировкой стека для decor2
. На этот раз актуален только кадр 2.
print("2==================") f = stack[2][0] for k,v in f.f_locals.items(): if callable(v) and k in f.f_code.co_freevars: print(k, v.__name__, v.__code__) print("==================")
Выход
2================== ('inner_fn2', 'caller', <code object wrapper1 at 0x1070db630, file "decorators.py", line 35>) ==================
Эммм, хорошо, имя функции действительно caller
, но имя файла “decorators.py”
.
Так как сложенные декораторы эквивалентны
decorator.decor2(decorator.decor1(callers.caller))()
Несмотря на то, что caller()
определено в callers.py
, decor2
получает объект caller
через inner_fn2
из decor1
. Глядя на строку 35, мы можем подтвердить, что inner_fn1
здесь caller
.
# decorators.py ... 34 def decor1(inner_fn1): 35 @wraps(inner_fn1) 36 def wrapper1(*args, **kwargs): 37 func1() 38 return inner_fn1(*args, **kwargs) 39 return wrapper1
Что теперь?
С этого момента все становится немного сложнее. Возвращаемый объект decor1
— это wrapper1
, который является замыканием и должен содержать свободные переменные из объемлющей области.
Это можно проверить, перебирая атрибут .__closure__
или .co_freevars
для имен свободных переменных.
def print_stack(): stack = inspect.stack() pp.pprint(stack) print("2==================") f = stack[2][0] for k,v in f.f_locals.items(): if callable(v) and k in f.f_code.co_freevars: print(k, v.__name__, v.__code__) try: closure = v.__closure__ print(closure) except: continue if closure: for c in closure: if callable(c) print("free var", c.cell_contents) print(c.cell_contents.__code__.co_name) print(c.cell_contents.__code__.co_filename)
Выход
('inner_fn2', 'caller', <code object wrapper1 at 0x1068ef6b0, file "decorators.py", line 35>) (<cell at 0x106862980: function object at 0x106873050>,) ('free var', <function caller at 0x106873050>) caller callers.py
Мы можем видеть, что при использовании нескольких декораторов целевой вызывающий объект может быть получен с помощью следующих шагов:
- проверить локальную область фрейма на наличие вызываемых типов
- перебрать
cell_contents
поля__closure__
вызываемого объекта - если
cell_contents
является вызываемым объектом с непустым__closure__
, нам нужно рекурсивно проверить его содержимое.
Конечно, если целевой вызывающий объект был найден на каком-либо шаге, мы останавливаемся и возвращаем вызывающий объект. Когда остановиться, зависит от требований.
Для моей цели я создаю белый список, чтобы игнорировать любые кадры в стеке вызовов, которые мне не нужны, и алгоритм завершится, как только увидит вызывающего абонента, который не входит в белый список.
Разве
inspect.unwrap()
не сделал бы то же самое?
Я думаю, что это очень возможно. inspect.unwrap()
не существует в Python 2. В Python 3 это зависит от использования functools.wraps().
Представление?
Я заметил значительное снижение производительности при использовании модуля inspect
. Базовая реализация inspect
использует sys._getframe()
. Использование sys._getframe().f_back
для перехода на следующий уровень в стеке примерно на один или два порядка быстрее, и настоятельно рекомендуется, если производительность при выполнении этой трассировки имеет решающее значение для вашего приложения.
Чтобы лучше понять декораторов, я нашел это очень длинное, но отличное чтение. Это лучшее объяснение декораторов и замыканий, которое я когда-либо читал.
https://towardsdatascience.com/closures-and-decorators-in-python-2551abbc6eb6