为什么Python日志记录模块的源代码在try中引发异常



在阅读python日志模块的源代码时,我发现了这样的代码:

# next bit filched from 1.5.2's inspect.py
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
raise Exception
except:
return sys.exc_info()[2].tb_frame.f_back

为什么?它等于这个吗?

def currentframe():
return sys.exc_info()[2].tb_frame.f_back

根据sys.exc_info的官方文档,您需要在任何堆栈帧中出现异常才能获得(type, value, traceback)的元组。如果没有处理异常,则会得到一个具有None值的元组。堆栈帧可以是:当前堆栈、函数的调用堆栈或调用方(函数)本身。在日志记录中,我们只关心当前堆栈的traceback(注意sys.exc_info()[2]),因此必须引发异常才能访问元组值。以下是文档摘录:

此函数返回一个由三个值组成的元组,这些值提供有关当前正在处理的异常的信息。返回的信息是特定于当前线程和当前堆栈帧的。如果当前堆栈帧没有处理异常,则从调用堆栈帧或其调用方获取信息,依此类推,直到找到正在处理异常的堆栈帧。在这里,"处理异常"被定义为"执行exception子句"。对于任何堆栈帧,只有关于当前正在处理的异常的信息是可访问的。

如果堆栈上的任何位置都没有处理异常,则元组返回包含三个None值的值。否则,值返回的是(类型、值、回溯)。他们的意思是:类型获得正在处理的异常的类型(BaseException的子类);value获取异常实例(异常类型的实例);traceback获取一个traceback对象(请参阅参考手册),该对象在异常所在的点封装调用堆栈最初发生。

sys_getframe([deith])从调用堆栈返回帧对象。如果给定可选整数depth,则返回堆栈顶部以下许多调用的帧对象。深度的默认值为零,返回调用堆栈顶部的帧。

另一个需要考虑的重要点是不能保证该函数存在于Python的所有实现中。我们知道CPython有它。下面来自logging/__init__.py的代码执行此检查。请注意,currentframe()是lambda函数

if hasattr(sys, '_getframe'):
currentframe = lambda: sys._getframe(3)

这意味着:如果sys_getframe()存在于Python实现中,从调用堆栈的顶部返回第三个帧对象。如果sys没有此函数作为属性,则下面的else语句将引发一个Exception,以从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代码来构建一个示例(并非双关语)。这是受到这里出色的解释的启发。以下示例包含保存在名为main.py的文件中的3个函数。

#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__.pyif-else语句的相同代码。唯一的区别是,我们将depth参数x传递给函数,该函数由lambda函数用于获取depth:currentframe = lambda: sys._getframe(x)处的帧对象。

  • 函数show_frame(num,frame):此函数print帧对象,具有深度的帧函数调用sys._getframe(num),调用方函数名例如show_frame()。。执行调用函数代码的文件的文件名以及调用函数代码中的当前行号等。CCD_ 22是CCD_ 23返回的帧对象的属性。co_name是此代码对象的一个属性,并返回定义代码对象时使用的名称(可以打印f_code进行检查)。类似地,co_filename检索文件名,f_lineno检索当前行号。您可以在inspect文档中找到这些属性的解释,该文档也用于有趣地获取框架对象。您还可以编写一些独立的代码来了解这些属性是如何工作的。例如,下面的代码获取当前帧frameobj(即:堆栈顶部的帧对象,深度0(默认值)),并打印该帧的代码对象的文件名(我在main_module.py中运行此代码)。

    import sys
    frameobj = sys._getframe()
    print(frameobj.f_code.co_filename)
    #output:
    main_module.py
    

    调用堆栈不太深,因为只有一个函数调用CCD_ 30。如果我们更改代码以获得深度为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()

调用test()时,调用堆栈为:test-->get_current_frame-->show_frame。在随后的调用中,堆栈为get_current_frame--->show_frame,直到for循环完成test()中的范围(4)。如果我们检查顶部的输出,堆栈顶部的帧的深度为0:CCD_ 37,并且调用函数是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才能到达调用方函数的堆栈帧。

我们也可以用前面的玩具例子来理解这个概念。由于堆栈不太深,我们得到深度0的当前帧。

import sys
frameobj = sys._getframe()
print(frameobj.f_code.co_name)
#Output:
<module>

现在,如果我的Python实现没有sys_getframe()属性,该怎么办?在这种情况下,else中的代码将执行并引发一个Exception以从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

CCD_ 44返回当前Exception返回的回溯帧CCD_。我们可以通过打印返回语句来检查这一点: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---->

上面的函数获取调用方函数的当前帧,并在稍后使用这些信息来获取我们之前访问的相同属性(文件名等)。

显而易见-在出现异常之前,没有exc_info(异常信息)。所以我们需要引发异常来访问它的信息并从中获取调用堆栈。

这似乎是访问当前调用堆栈的最简单方法。

最新更新