我想记录一个请求在请求末尾访问过一次的所有方法,以进行调试。
我可以先从一个课程开始:
这是我想要的输出示例:
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.deco
的wrapper
函数将使用此属性来跟踪已调用但未结束的方法数量。利用该信息,它可以适当缩进打印的方法名称。还要注意,我们要通过
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/