首页 > 解决方案 > python - 记录请求的旅程

问题描述

我想在请求结束时记录单个请求访问过的所有方法,以进行调试。

我可以从一开始只上课:

这是我想要的输出示例:


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

标签: pythondebuggingstack-trace

解决方案


方法

对于这个问题,我认为有两种不同的方法值得考虑:

  1. “简单”日志记录元类,或
  2. 更强大的元类来存储调用堆栈

如果您只需要在进行方法调用时打印它们,并且您不关心保存方法调用堆栈的实际记录,那么第一种方法应该可以解决问题。

我不确定您正在寻找哪种方法(如果您有任何特定的想法),但如果您知道除了打印调用之外还需要存储方法调用堆栈,您可能需要跳到第二个方法。

注意:以下所有代码都假定存在以下导入:

from types import FunctionType

1. 简单的日志元类

这种方法要容易得多,并且在您第一次尝试时不需要太多额外的工作(取决于我们要考虑的特殊情况)。然而,正如已经提到的,这个元类只关心日志记录。如果您确实需要保存方法调用堆栈结构,请考虑跳到第二种方法。

更改为DecoMeta.__new__

使用这种方法,您的DecoMeta.__new__方法基本保持不变。下面代码中最显着的变化是将“_in_progress_calls”列表添加到namespace. DecoMeta.decowrapper函数将使用此属性来跟踪已调用但未结束的方法的数量。使用该信息,它可以适当地缩进打印的方法名称。

还要注意我们要通过 装饰staticmethod的属性。但是,您可能不需要此功能。另一方面,您可能还需要考虑通过会计和其他方式走得更远。namespaceDecoMeta.decoclassmethod

您会注意到的另一项更改是变量的创建,该cls变量在返回之前直接进行了修改。但是,您现有的通过命名空间的循环,然后是类对象的创建和返回,仍然应该在这里解决问题。

更改为DecoMeta.deco

  1. 我们将in_progress_calls当前实例设置为_in_progress_calls稍后使用wrapper

  2. 接下来,我们对您第一次处理的尝试进行小修改staticmethod- 如前所述,您可能想要也可能不想要

  3. 在“Log”部分,我们需要计算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): ...

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一个 dict 。请注意,在上述两种问题情况下,这都会悄然失败;但是,它确实有助于说明如果您决定更进一步所必需的更新。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;博士

如果我正确地将您的问题解释为要求一个真正只记录方法调用的元类,那么第一种方法(在上面的“简单日志记录元类”标题下概述)应该很好用。但是,如果您还需要保存所有方法调用的完整记录,则可以按照“Beefy Metaclass to Store Call Stacks”标题下的建议开始。

如果您有任何其他问题或澄清,请告诉我。我希望这很有用!


推荐阅读