我想记录一个请求在请求末尾访问过一次的所有方法,以进行调试。

我可以先从一个课程开始:

这是我想要的输出示例:


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

最佳答案

方法

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


“简单”日志记录元类,或者
Beefier元类可存储调用堆栈


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

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

注意:此后所有代码均假定存在以下导入:

from types import FunctionType


1.简单的记录元类

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

DecoMeta.__new__的更改

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

还要注意,我们要通过staticmethod装饰的namespace属性中包含DecoMeta.deco。但是,您可能不需要此功能。另一方面,您可能还想考虑进一步考虑classmethod和其他因素。

您会注意到的另一项更改是创建了cls变量,该变量将在返回之前直接进行修改。但是,您现有的遍历命名空间的循环,以及随后创建和返回类对象的操作,仍然可以解决问题。

DecoMeta.deco的更改


我们将in_progress_calls设置为当前实例的_in_progress_calls,以便稍后在wrapper中使用
接下来,我们对您第一次尝试处理staticmethod的方法进行了一些小的修改-如前所述,您可能想要或不想要的东西
在“日志”部分,我们需要为以下行计算pad,在此行中打印被调用方法的name。打印后,我们将当前方法name添加到in_progress_calls,通知其他方法进行中的方法
在“调用方法”部分,我们(可选)再次处理staticmethod

除了这一小的更改,我们还通过在self调用中添加func参数来进行了一个很小但重要的更改。没有这个,使用DecoMeta的类的常规方法将开始抱怨没有给出位置self参数,这很重要,因为func.__call__method-wrapper且需要实例我们的方法是绑定的。
第一次尝试的最后更改是删除最后一个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. Beefy元类存储调用堆栈

因为我不确定您是否真正想要这个,而且您的问题似乎更着重于问题的元类部分,而不是调用堆栈存储结构,所以我将集中于如何增强上述元类以处理所需的操作。然后,我将简单介绍一下存储调用堆栈并使用简单的占位符结构“存根”代码的那些部分的多种方式。

我们需要的显而易见的事情是一个持久的调用堆栈结构,以扩展临时_in_progress_calls属性的范围。因此,我们可以在DecoMeta.__new__的顶部添加以下未注释的行:

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


不幸的是,显而易见性到此为止,如果您想跟踪非常简单的方法调用堆栈之外的任何内容,事情会很快变得棘手。

关于我们如何保存调用堆栈,有一些事情可能会限制我们的选择:


我们不能使用以方法名称作为键的简单dict,因为在生成的任意复杂的调用堆栈中,方法X很有可能多次调用方法Y。
我们不能假设对方法X的每次调用都会调用相同的方法,正如您的“有条件”方法示例所表明的那样。这意味着我们不能说对X的任何调用都会产生调用堆栈Y,并将该信息整齐地保存在某个地方
我们需要限制新的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元类”标题下的建议开始。

如果您还有其他问题或疑问,请告诉我。我希望这是有用的!

关于python - python-记录请求的旅程,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/55902280/

10-12 23:52