python - записать ход запроса - PullRequest
2 голосов
/ 29 апреля 2019

Я хочу зарегистрировать все методы, которые один запрос посетил один раз в конце запроса для целей отладки.

Я в порядке, начав сначала только с одного класса:

вот мой желаемый пример вывода:


logging full trace once
                      '__init__': ->
                                'init_method_1' ->
                                            'init_method_1_1' 
                                'init_method_2'
                      'main_function': ->
                                'first_main_function': ->
                                        'condition_method_3'
                                        'condition_method_5'

вот моя частичная попытка:

import types

class DecoMeta(type):
    def __new__(cls, name, bases, attrs):

        for attr_name, attr_value in attrs.items():
            if isinstance(attr_value, types.FunctionType):
                attrs[attr_name] = cls.deco(attr_value)

        return super(DecoMeta, cls).__new__(cls, name, bases, attrs)

    @classmethod
    def deco(cls, func):
        def wrapper(*args, **kwargs):

            name = func.__name__
            stacktrace_full.setdefault(name, [])
            sorted_functions = stacktrace_full[name]
            if len(sorted_functions) > 0:
                stacktrace_full[name].append(name)
            result = func(*args, **kwargs)
            print("after",func.__name__)
            return result
        return wrapper

class MyKlass(metaclass=DecoMeta):

1 Ответ

1 голос
/ 02 мая 2019

Подходы

Я думаю, что есть два разных подхода, которые стоит рассмотреть для этой проблемы:

  1. «Простой» метакласс регистрации, или
  2. Метакласс Beefier для хранения стеков вызовов

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

Я не уверен, какой подход вы ищете (если вы имели в виду что-то конкретное), но если вы знаете, что вам нужно хранить стек вызовов методов, помимо печати вызовов, вы можете пропустить ко второму подходу.

Примечание: весь код в дальнейшем предполагает наличие следующих импортов:

from types import FunctionType

1. Метакласс простого ведения журнала

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

Меняется на DecoMeta.__new__

При таком подходе ваш DecoMeta.__new__ метод остается в основном неизменным. Наиболее заметным изменением, внесенным в приведенный ниже код, является добавление списка «_in_progress_calls» в namespace. Функция DecoMeta.deco wrapper будет использовать этот атрибут для отслеживания того, сколько методов было вызвано, но не завершено. Обладая этой информацией, он может соответствующим образом сделать отступ для имен напечатанных методов.

Также обратите внимание на включение staticmethod в атрибуты namespace, которые мы хотим декорировать с помощью DecoMeta.deco. Однако вам может не понадобиться эта функциональность. С другой стороны, возможно, вы захотите пойти дальше, учтя classmethod и других, а также

.

Еще одно изменение, о котором вы заметите, - это создание переменной cls, которая изменяется непосредственно перед возвращением. Однако существующий цикл по пространству имен, за которым следуют как создание, так и возврат объекта класса, все равно должен сработать.

Меняется на DecoMeta.deco

  1. Мы установили in_progress_calls для текущего экземпляра _in_progress_calls, который будет использоваться позже в wrapper

  2. Далее мы внесем небольшое изменение в вашу первую попытку обработать staticmethod & mdash; то, что вы можете или не можете хотеть, как упоминалось ранее

  3. В разделе «Журнал» нам нужно вычислить pad для следующей строки, в которой мы печатаем name вызванного метода. После печати мы добавляем текущий метод name к in_progress_calls, информируя другие методы о текущем методе

  4. В разделе «Метод вызова» мы (опционально) снова обрабатываем staticmethod.

    Помимо этого незначительного изменения, мы вносим одно небольшое, но существенное изменение, добавляя аргумент self к нашему вызову func. Без этого обычные методы класса, использующие DecoMeta, начали бы жаловаться на то, что им не дан позиционный аргумент self, что довольно важно, поскольку func.__call__ является method-wrapper и требует экземпляра, для которого наш метод связан.

  5. Последнее изменение в вашей первой попытке - удалить последнее значение in_progress_calls, поскольку мы официально вызвали метод и возвращаем result

Заткнись и покажи мне код

class DecoMeta(type):
    def __new__(mcs, name, bases, namespace):
        namespace["_in_progress_calls"] = []
        cls = super().__new__(mcs, name, bases, namespace)

        for attr_name, attr_value in namespace.items():
            if isinstance(attr_value, (FunctionType, staticmethod)):
                setattr(cls, attr_name, mcs.deco(attr_value))
        return cls

    @classmethod
    def deco(mcs, func):
        def wrapper(self, *args, **kwargs):
            in_progress_calls = getattr(self, "_in_progress_calls")

            try:
                name = func.__name__
            except AttributeError:  # Resolve `staticmethod` names
                name = func.__func__.__name__

            #################### Log ####################
            pad = " " * (len(in_progress_calls) * 3)
            print(f"{pad}`{name}`")
            in_progress_calls.append(name)

            #################### Invoke Method ####################
            try:
                result = func(self, *args, **kwargs)
            except TypeError:  # Properly invoke `staticmethod`-typed `func`
                result = func.__func__(*args, **kwargs)

            in_progress_calls.pop(-1)
            return result
        return wrapper

Что это делает?

Вот некоторый код для фиктивного класса, который я пытался смоделировать после вывода желаемого примера:

Настройка

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

class MyKlass(metaclass=DecoMeta):
    def __init__(self):
        self.i_1()
        self.i_2()

    #################### Init Methods ####################
    def i_1(self):
        self.i_1_1()

    def i_1_1(self): ...
    def i_2(self): ...

    #################### Main Methods ####################
    def main(self, x):
        self.m_1(x)

    def m_1(self, x):
        if x == 0:
            self.c_1()
            self.c_2()
            self.c_4()
        elif x == 1:
            self.c_3()
            self.c_5()

    #################### Condition Methods ####################
    def c_1(self): ...
    def c_2(self): ...
    def c_3(self): ...
    def c_4(self): ...
    def c_5(self): ...

Run

my_k = MyKlass()
my_k.main(1)
my_k.main(0)

Консольный вывод

`__init__`
   `i_1`
      `i_1_1`
   `i_2`
`main`
   `m_1`
      `c_3`
      `c_5`
`main`
   `m_1`
      `c_1`
      `c_2`
      `c_4`

2. Мускулистый метакласс для хранения стеков вызовов

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

Нам нужна очевидная вещь - постоянная структура стека вызовов, расширяющая область действия эфемерного атрибута _in_progress_calls. Поэтому мы можем начать с добавления следующей некомментированной строки в начало DecoMeta.__new__:

namespace["full_stack"] = dict()
# namespace["_in_progress_calls"] = []
# cls = super().__new__(mcs, name, bases, namespace)
# ...

К сожалению, очевидность на этом заканчивается, и все становится довольно сложно, если вы хотите отследить что-либо, кроме очень простых стеков вызовов методов.

Относительно того, как нам нужно сохранить наш стек вызовов, есть несколько вещей, которые могут ограничить наши параметры:

  1. Мы не можем использовать простой dict с именами методов в качестве ключей, потому что в результирующем произвольно сложном стеке вызовов вполне возможно, что метод X может вызывать метод Y несколько раз
  2. Мы не можем предполагать, что каждый вызов метода X будет вызывать одни и те же методы, как показывает ваш пример с «условными» методами. Это означает, что мы не можем сказать, что любой вызов X приведет к стеку вызовов Y и аккуратно сохранит эту информацию где-нибудь
  3. Нам нужно ограничить постоянство нашего нового атрибута full_stack, так как мы объявляем его для всего класса в DecoMeta.__new__. Если мы этого не сделаем, то все экземпляры MyKlass будут иметь один и тот же full_stack, быстро подрывая его полезность

Поскольку первые два в значительной степени зависят от ваших предпочтений / требований и поскольку я думаю, что ваш вопрос больше касается аспекта метакласса проблемы, а не структуры стека вызовов, я начну с рассмотрения третьего пункта.

Чтобы каждый экземпляр получал свой собственный full_stack, мы можем добавить новый метод DecoMeta.__call__, который вызывается всякий раз, когда мы создаем экземпляр MyKlass (или что-либо, использующее DecoMeta в качестве метакласса). Просто добавьте в DecoMeta:

def __call__(cls, *args, **kwargs):
    setattr(cls, "full_stack", dict())
    return super().__call__(*args, **kwargs)

Последний шаг - выяснить, как вы хотите структурировать full_stack и добавить код, чтобы обновить его до функции DecoMeta.deco.wrapper.

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

Например, мы можем сделать full_stack диктовкой с ключами Tuple[str] и значениями List[str]. Имейте в виду, что в обоих вышеупомянутых проблемных условиях это может произойти сбой; тем не менее, он служит для иллюстрации обновлений, необходимых для DecoMeta.deco.wrapper, если вы решите пойти дальше.

Необходимо добавить только две строки:

Сначала сразу под подписью DecoMeta.deco.wrapper добавьте следующую некомментированную строку:

full_stack = getattr(self, "full_stack")
# in_progress_calls = getattr(self, "_in_progress_calls")
# ...

Во-вторых, в разделе «Журнал» сразу после вызова print добавьте следующую незакомментированную строку:

# print(f"{pad}`{name}`")
full_stack.setdefault(tuple(in_progress_calls), []).append(name)
# in_progress_calls.append(name)
# ...

TL; DR * * тысяча сто шестьдесят девять Если я правильно истолковал ваш вопрос как запрос метакласса, который на самом деле просто регистрирует вызовы методов, то первый подход (описанный выше под заголовком «Простой метакласс журналирования») должен работать отлично. Однако, если вам также необходимо сохранить полную запись всех вызовов методов, вы можете начать, следуя рекомендациям под заголовком «Beefy Metaclass to Store Stacks». Пожалуйста, дайте мне знать, если у вас есть другие вопросы или разъяснения. Я надеюсь, что это было полезно!

...