根据sys.exc_info 的官方文档,您需要任何堆栈帧中的异常来获取(type, value, traceback) 的元组。如果没有处理异常,您将获得一个带有 None 值的元组。堆栈帧可以是:当前堆栈,或函数的调用堆栈或调用者(函数)本身。在日志记录中,我们只关心当前堆栈的traceback(注意sys.exc_info()[2]),因此必须引发异常才能访问元组值。以下是文档的摘录:
此函数返回一个包含三个值的元组,这些值提供有关当前正在处理的异常的信息。返回的信息特定于当前线程和当前堆栈帧。如果当前堆栈帧未处理异常,则从调用堆栈帧或其调用者获取信息,依此类推,直到找到正在处理异常的堆栈帧。在这里,“处理异常”被定义为“执行一个 except 子句”。对于任何堆栈帧,只能访问有关当前正在处理的异常的信息。
如果堆栈上的任何地方都没有处理异常,则使用元组
返回包含三个 None 值。否则,值
返回的是(类型、值、回溯)。它们的意思是:类型获取
正在处理的异常类型(BaseException 的子类);
value 获取异常实例(异常类型的实例);
traceback 获取一个回溯对象(参见参考手册),该对象
将调用堆栈封装在异常发生的位置
最初发生。
sys._getframe([depth]) 从调用堆栈返回帧对象。如果给定可选整数 depth,则返回堆栈顶部以下多次调用的框架对象。深度的默认值为零,返回调用堆栈顶部的帧。
另一个需要考虑的重点是这个函数并不保证在所有的Python实现中都存在。我们知道 CPython 有它。来自logging/__init__.py 的以下代码执行此检查。请注意,currentframe() 是一个 lambda 函数。:
if hasattr(sys, '_getframe'):
currentframe = lambda: sys._getframe(3)
这意味着:如果 Python 实现中存在 sys._getframe(),则返回调用堆栈顶部的第 3 帧对象。如果sys 没有此函数作为属性,则下面的else 语句会引发异常以从Traceback 中捕获框架对象。
else: #pragma: no cover
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
raise Exception
except Exception:
return sys.exc_info()[2].tb_frame.f_back
为了更好地理解这个概念,我使用上面的if-else 代码来构建一个示例(不是双关语)。这是受到出色解释here 的启发。以下示例包含 3 个函数,它们保存在名为 main.py 的文件中。
#main.py
def get_current_frame(x):
print("Reached get_current_frame")
if hasattr(sys, '_getframe'):
currentframe = lambda x: sys._getframe(x)
else: #pragma: no cover
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
raise Exception
except Exception:
return sys.exc_info()[2].tb_frame.f_back
return currentframe
def show_frame(num, frame):
print("Reached show_frame")
print(frame)
print(" frame = sys._getframe(%s)" % num)
print(" function = %s()" % frame(num).f_code.co_name)
print(" file/line = %s:%s" % (frame(num).f_code.co_filename, frame(num).f_lineno))
def test():
print("Reached test")
for num in range(4):
frame = get_current_frame(num)
show_frame(num, frame)
#function call
test()
在使用python main.py 运行此代码时,我们得到以下输出:
Reached test
Reached get_current_frame
Reached show_frame
<function get_current_frame.<locals>.<lambda> at 0x0000000002EB0AE8>
frame = sys._getframe(0)
function = <lambda>()
file/line = main.py:74
Reached get_current_frame
Reached show_frame
<function get_current_frame.<locals>.<lambda> at 0x0000000002EB0B70>
frame = sys._getframe(1)
function = show_frame()
file/line = main.py:96
Reached get_current_frame
Reached show_frame
<function get_current_frame.<locals>.<lambda> at 0x0000000002EB0AE8>
frame = sys._getframe(2)
function = test()
file/line = main.py:89
Reached get_current_frame
Reached show_frame
<function get_current_frame.<locals>.<lambda> at 0x0000000002EB0B70>
frame = sys._getframe(3)
function = <module>()
file/line = main.py:115
说明:
函数 get_current_frame(x):此函数包含来自 logging/__init__.py 的 if-else 语句中的相同代码。唯一的区别是我们将depth 参数x 传递给lambda 函数用来在该depth 处抓取框架对象的函数:@ 987654346@.
-
Function show_frame(num, frame):这个函数prints frame 对象,带有depth的frame函数调用, sys._getframe(num),调用者函数名,例如。 show_frame()..等等。 , 执行调用函数代码的文件的文件名以及当前行号。在调用函数的代码中。 f_code是sys._getframe()返回的frame对象的一个属性,是一个code对象。 co_name 是此代码对象的一个属性,并返回定义代码对象的名称(您可以打印 f_code 来检查这一点)。同样,co_filename 检索文件名,f_lineno 检索当前行号。您可以在 inspect 文档中找到这些属性的解释,该文档也用于有趣地获取框架对象。您还可以编写一些独立的代码来了解这些属性是如何工作的。例如。下面的代码获取当前帧frameobj(即:堆栈顶部的帧对象,深度0(默认))并打印该帧的代码对象的文件名(我在@987654357中运行此代码@)。
import sys
frameobj = sys._getframe()
print(frameobj.f_code.co_filename)
#output:
main_module.py
调用堆栈不是太深,因为只有一个函数调用
_getframe()。如果我们更改代码以获取深度 1 的帧,我们会得到一个
错误:
Traceback (most recent call last):
File "main_module.py", line 3, in <module>
frameobj = sys._getframe(1)
ValueError: call stack is not deep enough
Function test():该函数获取当前帧对象的深度num在某个范围内,然后为该num和帧对象调用show_frame()。 p>
当test()被调用时,调用栈为:test --> get_current_frame --> show_frame。在随后的调用中,堆栈为 get_current_frame ---> show_frame,直到 for 循环完成 test() 中的 range(4)。如果我们从顶部检查输出,堆栈顶部的帧的深度为 0:
frame = sys._getframe(0) 调用函数是 lambda 函数本身。行号file/line = main.py:74 中的 74 是当前行号。当这个函数被调用时(想象它就像那个帧的最后一个光标位置)。最后,我们查看堆栈底部的框架。这也是用于记录的框架对象(深度为 3):
Reached get_current_frame
Reached show_frame
<function get_current_frame.<locals>.<lambda> at 0x0000000002EB0B70>
frame = sys._getframe(3)
function = <module>()
file/line = main.py:115
在日志中,我们需要 3 的深度才能到达调用函数的堆栈帧。
我们也可以使用我们之前的玩具示例来理解这个概念。由于堆栈不是太深,我们在 depth 0 处获取当前帧。
import sys
frameobj = sys._getframe()
print(frameobj.f_code.co_name)
#Output:
<module>
现在,如果我的 Python 实现没有 sys 的 _getframe() 属性怎么办?在这种情况下,else 中的代码将执行并引发异常以从traceback 获取当前帧。以下函数执行此操作,此处的调用函数再次为<module>(注意输出):
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
# test = 'x' + 1
raise Exception
except Exception:
_type, _value, _traceback = sys.exc_info()
print("Type: {}, Value:{}, Traceback:{}".format(_type, _value, _traceback))
print("Calling function:{}, Calling file: {}".format(sys.exc_info()[2].tb_frame.f_back.f_code.co_name, sys.exc_info()[2].tb_frame.f_back.f_code.co_filename))
return sys.exc_info()[2].tb_frame.f_back
currentframe()
#Output:
Type: <class 'Exception'>, Value:, Traceback:<traceback object at 0x0000000002EFEB48>
Calling function:<module>, Calling file: main.py
f_back 返回当前 Exception 返回的回溯帧 tb_frame 的帧对象。我们可以通过打印返回语句来检查这一点:print(sys.exc_info()[2].tb_frame.f_back),我们会得到类似:<frame object at 0x000000000049B2C8>
这解释了日志记录模块如何捕获当前帧。
那么,currentframe() 后来在日志源代码中使用在哪里呢?你会在这里找到它:
def findCaller(self, stack_info=False):
"""
Find the stack frame of the caller so that we can note the source
file name, line number and function name.
"""
f = currentframe()
#<----code---->
上述函数获取调用者函数的当前帧,稍后使用此信息获取我们之前访问的相同属性(文件名等)。