Я имел в виду нечто очень простое, как это:
#the decorator
def debug_function(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print('debug:', res)
return res
return wrapper
#proof of concept:
@debug_function
def square(number):
return number*number
class ClassA:
def __init__(self):
self.Number = 42
@debug_function
def return_number(self):
return self.Number
if __name__ == '__main__':
result = [square(i) for i in range(5)]
print(result)
my_obj = ClassA()
n = my_obj.return_number()
print(n)
Короче говоря, напишите простой декоратор, который записывает результат вашей функции где-нибудь (выше я только записываю его в терминал, но это может быть расширено для использования файла журнала или аналогичного). Затем вы декорируете любую функцию, которую хотите отслеживать, и получите ее возвращаемое значение при каждом вызове функции. В приведенном выше коде я показываю, что он делает для простой функции и метода класса. Результат примера кода выглядит следующим образом:
debug: 0
debug: 1
debug: 4
debug: 9
debug: 16
[0, 1, 4, 9, 16]
debug: 42
42
РЕДАКТИРОВАТЬ 2 :
Я отредактировал код ниже, чтобы использовать реальную функцию вместо просто ее __name__
для хранения промежуточных значений. Это должно сделать его менее подверженным ошибкам.
EDIT
Для хранения значений в памяти я бы снова пошел как можно проще и просто сохранил значения в списке. Для простого примера, описанного выше, возможно, будет достаточно глобального объекта списка. Однако, поскольку вы, скорее всего, захотите просматривать более одной функции за раз, я бы предпочел разработать декоратор как класс и сохранить один список для каждой функции в атрибуте класса. Подробнее об этом в примере кода.
Настоящая проблема - хранение локальных переменных. Для этого вам нужно изменить фактический код вашей функции. Естественно, вы не хотите делать это «вручную», но хотите, чтобы об этом позаботился ваш декоратор. Здесь становится сложно. Посмотрев некоторое время, я нашел пакет с именем bytecode (который работает по крайней мере для Python 3.6). Скорее всего, есть другие варианты, но я решил пойти с этим. bytecode
позволяет вам переводить байт-код python в удобочитаемую форму, изменять его и переводить обратно в байт-код python. Я должен признать, что здесь я немного не в себе, но я написал несколько небольших функций, посмотрел на переведенный код и разработал фрагмент кода, который делает то, что я хочу.
Таким образом, в этом примере цель состоит в том, чтобы украсить тестируемую функцию так, чтобы декоратор принял список строк в качестве аргумента, где каждая строка - это имя переменной, которая должна отслеживаться. Затем он добавляет код в тело функции, который упаковывает окончательные значения всех перечисленных переменных в кортеж и возвращает кортеж вместе с реальным возвращаемым значением. Затем функция «обертка» собирает отслеживаемые значения и добавляет их в специфический для функции список значений, который можно прочитать в любой точке кода.
Так что вот так. Поместите фактический декоратор в его собственный файл, я называю это здесь debug_function.py
:
from bytecode import Bytecode, Instr
class debug_function(object):
"""
Decorator that takes a list of variable names as argument. Everytime
the decorated function is called, the final states of the listed
variables are logged and can be read any time during code execution.
"""
_functions = {}
def __init__(self, varnames):
self.varnames = varnames
def __call__(self, func):
print('logging variables {} of function {}'.format(
','.join(self.varnames), func.__name__
))
debug_function._functions[func] = []
c = Bytecode.from_code(func.__code__)
extra_code = [
Instr('STORE_FAST', '_res')
]+[
Instr('LOAD_FAST', name) for name in self.varnames
]+[
Instr('BUILD_TUPLE', len(self.varnames)),
Instr('STORE_FAST', '_debug_tuple'),
Instr('LOAD_FAST', '_res'),
Instr('LOAD_FAST', '_debug_tuple'),
Instr('BUILD_TUPLE', 2),
Instr('STORE_FAST', '_result_tuple'),
Instr('LOAD_FAST', '_result_tuple'),
]
c[-1:-1]= extra_code
func.__code__=c.to_code()
def wrapper(*args, **kwargs):
res, values = func(*args, **kwargs)
debug_function._functions[func].append(values)
return res
return wrapper
@staticmethod
def get_values(func):
return debug_function._functions[func]
Затем, давайте снова сгенерируем некоторые функции для проверки, которые мы украсим этим декоратором. Поместите их, например, в functions.py
from debug_function import debug_function
@debug_function(['c','d'])
def test_func(a,b):
c = a+b
d = a-b
return c+d
class test_class:
def __init__(self, value):
self.val = value
@debug_function(['y'])
def test_method(self, *args):
x = sum(args)
y = 1
for arg in args:
y*=arg
return x+y
Наконец, вызовите функции и посмотрите на вывод. debug_function
имеет статический метод с именем get()
, который принимает функцию, о которой вы хотите получить информацию, в качестве аргумента и возвращает список кортежей. Каждый из этих кортежей содержит окончательные значения всех локальных переменных, которые вы хотите отслеживать после одного вызова этой функции. Значения находятся в том же порядке, в котором они были перечислены в операторе декоратора. С 'inverse' zip
вы можете легко разделить эти кортежи.
from debug_function import debug_function
from functions import test_func, test_class
results = [test_func(i,j) for i in range(5) for j in range(8,12)]
c,d = zip(*debug_function.get_values(test_func))
print('results:', results)
print('intermediate values:')
print('c =', c)
print('d =', d)
my_class = test_class(7)
results2 = [
my_class.test_method(i,j,4,2) for i in range(5) for j in range(8,12)
]
y, = zip(*debug_function.get_values(test_class.test_method))
print('results:', results2)
print('intermediate values:')
print('y =', y)
Вывод вызовов выглядит следующим образом:
logging variables c,d of function test_func
logging variables y of function test_method
results: [0, 0, 0, 0, 2, 2, 2, 2, 4, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8]
intermediate values:
c = (8, 9, 10, 11, 9, 10, 11, 12, 10, 11, 12, 13, 11, 12, 13, 14, 12, 13, 14, 15)
d = (-8, -9, -10, -11, -7, -8, -9, -10, -6, -7, -8, -9, -5, -6, -7, -8, -4, -5, -6, -7)
results: [14, 15, 16, 17, 79, 88, 97, 106, 144, 161, 178, 195, 209, 234, 259, 284, 274, 307, 340, 373]
intermediate values:
y = (0, 0, 0, 0, 64, 72, 80, 88, 128, 144, 160, 176, 192, 216, 240, 264, 256, 288, 320, 352)
Возможно, мне следует немного лучше объяснить, как это работает, пожалуйста, спросите, осталось ли что-то неясным. Как сказано выше, этот декоратор хранит только окончательное значение каждой переменной (то есть значение, которое переменная имеет после кода функции был выполнен). Если у вас есть более сложная функция, вам может быть интересно, какое значение имеет, например, каждое присвоение переменной - в этом случае вам придется проделать немного больше работы, но это должно быть выполнимо.
Надеюсь, это поможет