Как я могу отслеживать значения локальной переменной в Python? - PullRequest
0 голосов
/ 13 сентября 2018

Мой алгоритм зацикливается на секунды данных. Для каждой второй ценности данных я возвращаю значение, которое является результатом довольно сложного вычисления с участием нескольких подмодулей и подклассов. Некоторые из этих классов переинициализируются каждую секунду.

В целях отладки я несколько раз оказывался в ситуации, когда мне хотелось со временем отобразить значение определенной локальной переменной в одном из этих классов. Что-то внешнее должно было бы сериализовать и записать значения, потому что класс существует только секунду. Это была другая локальная переменная каждый раз.

Как я могу достичь своей цели должным образом с точки зрения разработки программного обеспечения, без того, чтобы это занимало у меня часы каждый раз и без написания более чем строки или двух нового кода каждый раз, когда я хочу это сделать?

В идеале, одним из решений, которое я рассмотрел, было бы иметь что-то вроде глобального потока ввода-вывода или что-то в этом роде, чтобы я «просто был рядом» без необходимости инициализировать каждый класс с ним, поэтому я мог бы просто вставить какой-то команды MySerializer << [mylocalvariable, timestamp] в любой точке кода, а затем, когда выполнение закончится, я могу проверить, пуст ли MySerializer и, если нет, я могу нарисовать, что в нем ... или что-то в этом роде. Еще лучше, если бы я мог сделать это для нескольких локальных переменных в разных классах. Будет ли это решение хорошо? Как я мог это сделать?

Или, чтобы иметь возможность сделать это аспектно-ориентированным способом, когда некоторый внешний объект «смотрит на код», не изменяя его, создает буфер значений этой локальной переменной и выплевывает их в график в конец. Как я могу это сделать?

Есть ли лучшее решение, чем любой из них? Какие шаблоны дизайна подходят для этой ситуации?

Что я делал в прошлом, так это возвращал эту локальную переменную тому, кто держит функцию, который затем, в свою очередь, должен возвращать полученное ею значение и так далее, и так далее, вплоть до самого верха. , Это огромный беспорядок, и каждый раз его нужно писать и удалять.

1 Ответ

0 голосов
/ 13 сентября 2018

Я имел в виду нечто очень простое, как это:

#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)

Возможно, мне следует немного лучше объяснить, как это работает, пожалуйста, спросите, осталось ли что-то неясным. Как сказано выше, этот декоратор хранит только окончательное значение каждой переменной (то есть значение, которое переменная имеет после кода функции был выполнен). Если у вас есть более сложная функция, вам может быть интересно, какое значение имеет, например, каждое присвоение переменной - в этом случае вам придется проделать немного больше работы, но это должно быть выполнимо.

Надеюсь, это поможет

...