python - 记录请求的旅程
python - log the request's journey
我想在请求结束时记录单个请求访问过的所有方法,以便进行调试。
我可以先从一个 class 开始:
这是我想要的输出示例:
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):
方法
我认为有两种不同的方法值得考虑解决这个问题:
- "Simple" 记录元class,或
- 更强大的元数据class 用于存储调用堆栈
如果您只需要打印方法调用,而不关心保存方法调用堆栈的实际记录,那么第一种方法应该可以解决问题。
我不确定您正在寻找哪种方法(如果您有任何具体的想法),但是如果您知道除了打印调用之外还需要存储方法调用堆栈,您可能想要向前跳到第二种方法。
注意:此后所有代码均假定存在以下导入:
from types import FunctionType
1。简单日志记录元class
这种方法要简单得多,并且不需要在您第一次尝试的基础上做太多额外的工作(取决于我们要考虑的特殊情况)。然而,正如已经提到的,这个 metaclass 只与日志有关。如果您确实需要保存方法调用堆栈结构,请考虑跳到第二种方法。
更改为 DecoMeta.__new__
使用这种方法,您的 DecoMeta.__new__
方法基本保持不变。下面代码中最显着的变化是将“_in_progress_calls”列表添加到 namespace
。 DecoMeta.deco
的 wrapper
函数将使用此属性来跟踪已调用但未结束的方法数。有了这些信息,它就可以适当地缩进打印的方法名称。
还要注意将 staticmethod
包含到我们要通过 DecoMeta.deco
修饰的 namespace
属性中。但是,您可能不需要此功能。另一方面,您可能需要考虑进一步考虑 classmethod
和其他因素。
您会注意到的另一个变化是 cls
变量的创建,它在被 returned 之前直接修改。但是,您现有的通过命名空间的循环,然后是 class object 的创建和 return 仍然应该在这里起作用。
更改为 DecoMeta.deco
我们将in_progress_calls
设置为当前实例的_in_progress_calls
,稍后在wrapper
中使用
接下来,我们对您第一次尝试处理 staticmethod
进行小幅修改 — 如前所述,您可能想要也可能不想要
在“Log”部分,我们需要为下一行计算pad
,我们在其中打印被调用方法的name
。打印后,我们将当前方法name
添加到in_progress_calls
,通知其他方法in-progress方法
在“调用方法”部分,我们(可选)再次处理 staticmethod
。
除了这个微小的变化之外,我们还进行了一个小而重要的变化,将 self
参数添加到我们的 func
调用中。没有这个,使用 DecoMeta
的 class 的普通方法将开始抱怨没有给出位置 self
参数,这是一个大问题,因为 func.__call__
是a method-wrapper
并且需要我们的方法绑定到的实例。
对您第一次尝试的最后更改是删除最后一个 in_progress_calls
值,因为我们已经正式调用该方法并且正在 returning 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 的代码,我试图根据您想要的示例输出进行建模:
设置
不要太在意这个街区。就是傻class他的方法调用其他方法
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 Metaclass
因为我不确定你是否真的想要这个,而且你的问题似乎更关注问题的元class部分,而不是调用堆栈存储结构,我将关注如何加强上述 metaclass 来处理所需的操作。然后,我将对存储调用堆栈的多种方法做一些说明,并使用简单的占位符结构“存根”代码的这些部分。
我们显然需要一个持久的调用堆栈结构来扩展临时 _in_progress_calls
属性的范围。因此,我们可以首先将以下未注释的行添加到 DecoMeta.__new__
的顶部:
namespace["full_stack"] = dict()
# namespace["_in_progress_calls"] = []
# cls = super().__new__(mcs, name, bases, namespace)
# ...
不幸的是,显而易见的事情到此为止,如果您想跟踪非常简单的方法调用堆栈以外的任何东西,事情很快就会变得棘手。
关于我们需要如何保存调用堆栈,有几件事可能会限制我们的选择:
- 我们不能使用简单的字典,以方法名称作为键,因为在生成的 arbitrarily-complex 调用堆栈中,方法 X 完全有可能 cll 方法 Y 多次
- 我们不能假设每次调用方法 X 都会调用相同的方法,正如您使用“条件”方法的示例所示。这意味着我们不能说对 X 的任何调用都会产生调用堆栈 Y,并在某处巧妙地保存该信息
- 我们需要限制新
full_stack
属性的持久性,因为我们在 DecoMeta.__new__
中基于 class-wide 声明它。如果我们不这样做,那么 MyKlass
的所有实例将共享相同的 full_stack
,并迅速破坏其有用性
因为前两个高度依赖于你的 preferences/requirements 并且因为我认为你的问题更关心问题的元class 方面,而不是调用堆栈结构,我将从解决第三点。
为了确保每个实例都有自己的 full_stack
,我们可以添加一个新的 DecoMeta.__call__
方法,每当我们创建 MyKlass
的实例(或任何使用 DecoMeta
作为元class)。只需将以下内容放入 DecoMeta
:
def __call__(cls, *args, **kwargs):
setattr(cls, "full_stack", dict())
return super().__call__(*args, **kwargs)
最后一块是弄清楚你想如何构造full_stack
并添加代码以将其更新到DecoMeta.deco.wrapper
函数。
一个deeply-nested字符串列表,按顺序命名调用的方法,以及这些方法调用的方法,等等......应该完成工作并回避上面提到的前两个问题, 但这听起来很乱,所以我会让你决定是否真的需要它。
例如,我们可以使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
如果我将你的问题解释为要求一个真正只记录方法调用的元class是正确的,那么第一种方法(在上面“简单记录元class”下概述标题)应该很好用。但是,如果您还需要保存所有方法调用的完整记录,您可以按照“Beefy Metaclass to Store Call Stacks”标题下的建议开始。
如果您有任何其他问题或需要说明,请告诉我。我希望这有用!
我想在请求结束时记录单个请求访问过的所有方法,以便进行调试。
我可以先从一个 class 开始:
这是我想要的输出示例:
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):
方法
我认为有两种不同的方法值得考虑解决这个问题:
- "Simple" 记录元class,或
- 更强大的元数据class 用于存储调用堆栈
如果您只需要打印方法调用,而不关心保存方法调用堆栈的实际记录,那么第一种方法应该可以解决问题。
我不确定您正在寻找哪种方法(如果您有任何具体的想法),但是如果您知道除了打印调用之外还需要存储方法调用堆栈,您可能想要向前跳到第二种方法。
注意:此后所有代码均假定存在以下导入:
from types import FunctionType
1。简单日志记录元class
这种方法要简单得多,并且不需要在您第一次尝试的基础上做太多额外的工作(取决于我们要考虑的特殊情况)。然而,正如已经提到的,这个 metaclass 只与日志有关。如果您确实需要保存方法调用堆栈结构,请考虑跳到第二种方法。
更改为 DecoMeta.__new__
使用这种方法,您的 DecoMeta.__new__
方法基本保持不变。下面代码中最显着的变化是将“_in_progress_calls”列表添加到 namespace
。 DecoMeta.deco
的 wrapper
函数将使用此属性来跟踪已调用但未结束的方法数。有了这些信息,它就可以适当地缩进打印的方法名称。
还要注意将 staticmethod
包含到我们要通过 DecoMeta.deco
修饰的 namespace
属性中。但是,您可能不需要此功能。另一方面,您可能需要考虑进一步考虑 classmethod
和其他因素。
您会注意到的另一个变化是 cls
变量的创建,它在被 returned 之前直接修改。但是,您现有的通过命名空间的循环,然后是 class object 的创建和 return 仍然应该在这里起作用。
更改为 DecoMeta.deco
我们将
in_progress_calls
设置为当前实例的_in_progress_calls
,稍后在wrapper
中使用
接下来,我们对您第一次尝试处理
staticmethod
进行小幅修改 — 如前所述,您可能想要也可能不想要在“Log”部分,我们需要为下一行计算
pad
,我们在其中打印被调用方法的name
。打印后,我们将当前方法name
添加到in_progress_calls
,通知其他方法in-progress方法在“调用方法”部分,我们(可选)再次处理
staticmethod
。除了这个微小的变化之外,我们还进行了一个小而重要的变化,将
self
参数添加到我们的func
调用中。没有这个,使用DecoMeta
的 class 的普通方法将开始抱怨没有给出位置self
参数,这是一个大问题,因为func.__call__
是amethod-wrapper
并且需要我们的方法绑定到的实例。对您第一次尝试的最后更改是删除最后一个
in_progress_calls
值,因为我们已经正式调用该方法并且正在 returningresult
闭嘴,给我看代码
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 的代码,我试图根据您想要的示例输出进行建模:
设置
不要太在意这个街区。就是傻class他的方法调用其他方法
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 Metaclass
因为我不确定你是否真的想要这个,而且你的问题似乎更关注问题的元class部分,而不是调用堆栈存储结构,我将关注如何加强上述 metaclass 来处理所需的操作。然后,我将对存储调用堆栈的多种方法做一些说明,并使用简单的占位符结构“存根”代码的这些部分。
我们显然需要一个持久的调用堆栈结构来扩展临时 _in_progress_calls
属性的范围。因此,我们可以首先将以下未注释的行添加到 DecoMeta.__new__
的顶部:
namespace["full_stack"] = dict()
# namespace["_in_progress_calls"] = []
# cls = super().__new__(mcs, name, bases, namespace)
# ...
不幸的是,显而易见的事情到此为止,如果您想跟踪非常简单的方法调用堆栈以外的任何东西,事情很快就会变得棘手。
关于我们需要如何保存调用堆栈,有几件事可能会限制我们的选择:
- 我们不能使用简单的字典,以方法名称作为键,因为在生成的 arbitrarily-complex 调用堆栈中,方法 X 完全有可能 cll 方法 Y 多次
- 我们不能假设每次调用方法 X 都会调用相同的方法,正如您使用“条件”方法的示例所示。这意味着我们不能说对 X 的任何调用都会产生调用堆栈 Y,并在某处巧妙地保存该信息
- 我们需要限制新
full_stack
属性的持久性,因为我们在DecoMeta.__new__
中基于 class-wide 声明它。如果我们不这样做,那么MyKlass
的所有实例将共享相同的full_stack
,并迅速破坏其有用性
因为前两个高度依赖于你的 preferences/requirements 并且因为我认为你的问题更关心问题的元class 方面,而不是调用堆栈结构,我将从解决第三点。
为了确保每个实例都有自己的 full_stack
,我们可以添加一个新的 DecoMeta.__call__
方法,每当我们创建 MyKlass
的实例(或任何使用 DecoMeta
作为元class)。只需将以下内容放入 DecoMeta
:
def __call__(cls, *args, **kwargs):
setattr(cls, "full_stack", dict())
return super().__call__(*args, **kwargs)
最后一块是弄清楚你想如何构造full_stack
并添加代码以将其更新到DecoMeta.deco.wrapper
函数。
一个deeply-nested字符串列表,按顺序命名调用的方法,以及这些方法调用的方法,等等......应该完成工作并回避上面提到的前两个问题, 但这听起来很乱,所以我会让你决定是否真的需要它。
例如,我们可以使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
如果我将你的问题解释为要求一个真正只记录方法调用的元class是正确的,那么第一种方法(在上面“简单记录元class”下概述标题)应该很好用。但是,如果您还需要保存所有方法调用的完整记录,您可以按照“Beefy Metaclass to Store Call Stacks”标题下的建议开始。
如果您有任何其他问题或需要说明,请告诉我。我希望这有用!