Следите за переменным изменением в python

Существует большой проект python, в котором один атрибут одного класса имеет неправильное значение в некотором месте.

Он должен быть sqlalchemy.orm.attributes.InstrumentedAttribute, но когда я запускаю тесты, это постоянное значение, пусть говорят string.

Есть ли способ запустить программу python в режиме отладки и запустить некоторую проверку (если переменная изменена) после каждого шага через строку кода автоматически?

P.S. Я знаю, как записывать изменения атрибута экземпляра класса с помощью инспектора и декоратора свойств. Возможно, здесь я могу использовать этот метод с метаклассами...

Но иногда мне нужно более общее и мощное решение...

Спасибо.

P.P.S. Мне нужно что-то вроде: qaru.site/info/154677/..., но может быть с более подробным объяснением того, что происходит в этом коде.

Ответ 1

Ну, вот что-то вроде медленного подхода. Он может быть изменен для просмотра локальной переменной (только по имени). Вот как это работает: мы делаем sys.settrace и анализируем значение obj.attr на каждом шаге. Сложная часть состоит в том, что мы получаем события 'line' (что некоторая строка была выполнена) до выполнения строки. Итак, когда мы замечаем, что obj.attr изменился, мы уже на следующей строке, и мы не можем получить предыдущий линейный фрейм (потому что кадры не копируются для каждой строки, они изменены). Поэтому в каждом событии строки я сохраняю traceback.format_stack до watcher.prev_st, и если при следующем вызове значения trace_command изменилось, мы печатаем сохраненную трассировку стека в файл. Сохранение трассировки на каждой строке - довольно дорогостоящая операция, поэтому вам нужно установить ключевое слово include в список ваших каталогов проектов (или только корень вашего проекта), чтобы не смотреть, как другие библиотеки делают свой материал и процессор отходов.

watcher.py

import traceback

class Watcher(object):
    def __init__(self, obj=None, attr=None, log_file='log.txt', include=[], enabled=False):
        """
            Debugger that watches for changes in object attributes
            obj - object to be watched
            attr - string, name of attribute
            log_file - string, where to write output
            include - list of strings, debug files only in these directories.
               Set it to path of your project otherwise it will take long time
               to run on big libraries import and usage.
        """

        self.log_file=log_file
        with open(self.log_file, 'wb'): pass
        self.prev_st = None
        self.include = [incl.replace('\\','/') for incl in include]
        if obj:
            self.value = getattr(obj, attr)
        self.obj = obj
        self.attr = attr
        self.enabled = enabled # Important, must be last line on __init__.

    def __call__(self, *args, **kwargs):
        kwargs['enabled'] = True
        self.__init__(*args, **kwargs)

    def check_condition(self):
        tmp = getattr(self.obj, self.attr)
        result = tmp != self.value
        self.value = tmp
        return result

    def trace_command(self, frame, event, arg):
        if event!='line' or not self.enabled:
            return self.trace_command
        if self.check_condition():
            if self.prev_st:
                with open(self.log_file, 'ab') as f:
                    print >>f, "Value of",self.obj,".",self.attr,"changed!"
                    print >>f,"###### Line:"
                    print >>f,''.join(self.prev_st)
        if self.include:
            fname = frame.f_code.co_filename.replace('\\','/')
            to_include = False
            for incl in self.include:
                if fname.startswith(incl):
                    to_include = True
                    break
            if not to_include:
                return self.trace_command
        self.prev_st = traceback.format_stack(frame)
        return self.trace_command
import sys
watcher = Watcher()
sys.settrace(watcher.trace_command)

testwatcher.py

from watcher import watcher
import numpy as np
import urllib2
class X(object):
    def __init__(self, foo):
        self.foo = foo

class Y(object):
    def __init__(self, x):
        self.xoo = x

    def boom(self):
        self.xoo.foo = "xoo foo!"
def main():
    x = X(50)
    watcher(x, 'foo', log_file='log.txt', include =['C:/Users/j/PycharmProjects/hello'])
    x.foo = 500
    x.goo = 300
    y = Y(x)
    y.boom()
    arr = np.arange(0,100,0.1)
    arr = arr**2
    for i in xrange(3):
        print 'a'
        x.foo = i

    for i in xrange(1):
        i = i+1

main()

Ответ 2

Вы можете использовать модуль отладки python (часть стандартной библиотеки)

Чтобы использовать, просто импортируйте pdb в начало исходного файла:

import pdb

а затем установите трассировку везде, где вы хотите начать проверку кода:

pdb.set_trace()

Затем вы можете выполнить код с помощью n и исследовать текущее состояние, выполнив команды python.

Ответ 4

Более простой способ наблюдать за изменением атрибута объекта (который также может быть переменной уровня модуля или чем-то, что доступно с помощью getattr) - использовать библиотеку hunter, гибкий инструментарий трассировки кода. Для обнаружения изменений состояния нам нужен предикат, который может выглядеть следующим образом:

import traceback


class MutationWatcher:

    def __init__(self, target, attrs):
        self.target = target
        self.state = {k: getattr(target, k) for k in attrs}

    def __call__(self, event):
        result = False
        for k, v in self.state.items():
            current_value = getattr(self.target, k)
            if v != current_value:
                result = True
                self.state[k] = current_value
                print('Value of attribute {} has chaned from {!r} to {!r}'.format(
                    k, v, current_value))

        if result:
            traceback.print_stack(event.frame)

        return result

Затем приведен пример кода:

class TargetThatChangesWeirdly:
    attr_name = 1


def some_nested_function_that_does_the_nasty_mutation(obj):
    obj.attr_name = 2


def some_public_api(obj):
    some_nested_function_that_does_the_nasty_mutation(obj)

Мы можем использовать его как hunter:

# or any other entry point that calls the public API of interest
if __name__ == '__main__':
    obj = TargetThatChangesWeirdly()

    import hunter
    watcher = MutationWatcher(obj, ['attr_name'])
    hunter.trace(watcher, stdlib=False, action=hunter.CodePrinter)

    some_public_api(obj)

Запуск модуля производит:

Value of attribute attr_name has chaned from 1 to 2
  File "test.py", line 44, in <module>
    some_public_api(obj)
  File "test.py", line 10, in some_public_api
    some_nested_function_that_does_the_nasty_mutation(obj)
  File "test.py", line 6, in some_nested_function_that_does_the_nasty_mutation
    obj.attr_name = 2
                                 test.py:6     return        obj.attr_name = 2
                                               ...       return value: None