Недавно мне пришлось работать над проектом, который включает в себя отслеживание стека вызовов 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

Мы можем видеть, что при использовании нескольких декораторов целевой вызывающий объект может быть получен с помощью следующих шагов:

  1. проверить локальную область фрейма на наличие вызываемых типов
  2. перебрать cell_contents поля __closure__ вызываемого объекта
  3. если 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