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

时间:2019-01-23 11:42:16

标签: python exception logging

在阅读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

2 个答案:

答案 0 :(得分:2)

根据sys.exc_info的官方文档,您需要在任何堆栈框架中都有一个异常才能获取(type, value, traceback)的元组。如果没有异常处理,您将得到一个具有None值的元组。堆栈框架可以是:当前堆栈,或者是函数的调用堆栈,或者是调用方(函数)本身。在日志记录中,我们仅关注当前堆栈的traceback(注意sys.exc_info()[2]),因此将不得不引发Exception才能访问元组值。这是摘录自文档:

  

此函数返回三个值的元组,它们给出有关当前正在处理的异常的信息。返回的信息特定于当前线程和当前堆栈帧。如果当前堆栈帧未处理异常,则从调用堆栈帧或其调用者等获取信息,依此类推,直到找到正在处理异常的堆栈帧为止。在这里,“处理异常”被定义为“执行except子句”。对于任何堆栈帧,只能访问有关当前正在处理的异常的信息。

     

如果堆栈上任何地方都没有异常处理,则为元组   包含三个无值的返回。否则,值   返回的是(类型,值,回溯)。它们的含义是:类型获取   处理的异常的类型(BaseException的子类);   值获取异常实例(异常类型的实例);   traceback获取一个traceback对象(请参阅参考手册),该对象   在异常处封装调用堆栈   最初发生。

sys._getframe([depth])从调用堆栈返回帧对象。如果给出了可选的整数 depth ,则返回在堆栈顶部下方多次调用的帧对象。深度的默认值为零,将帧返回到调用堆栈的顶部。

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

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

这意味着:如果python实现中存在sys._getframe(),则从调用堆栈的顶部返回第三个框架对象。如果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):该函数包含来自if-else的{​​{1}}语句的相同代码。唯一的区别是,我们将 depth 参数logging/__init__.py传递给该函数,x函数使用该参数以该 depth

抓取帧对象em>:lambda

  • 函数show_frame(num,frame):此函数currentframe = lambda: sys._getframe(x) frame对象,frame函数以其 depth进行调用print,调用方函数名称,例如。 sys._getframe(num) .. etc。 ,即执行调用功能代码的文件的文件名以及当前行号。在调用函数的代码中。 show_frame()f_code返回的框架对象的属性,并且是代码对象。 sys._getframe()是此代码对象的属性,并返回定义该代码对象的名称(您可以打印co_name进行检查)。同样,f_code检索文件名,co_filename检索当前行号。您可以在inspect文档中找到有关这些属性的说明,该说明还用于有趣地获取框架对象。您还可以编写一些隔离的代码来了解这些属性的工作方式。例如。下面的代码获取当前帧f_lineno(即:堆栈顶部的帧对象,深度0(默认值)),并为该帧打印代码对象的文件名(我正在运行此代码frameobj)。

    main_module.py

    调用栈不是太深,因为只有一个函数调用 import sys frameobj = sys._getframe() print(frameobj.f_code.co_filename) #output: main_module.py 。如果我们更改代码以使帧位于深度1,则会得到一个 错误:

    _getframe()
  • 功能测试():此功能获取深度 Traceback (most recent call last): File "main_module.py", line 3, in <module> frameobj = sys._getframe(1) ValueError: call stack is not deep enough 在某个范围内的当前帧对象,然后为该num调用show_frame()和框架对象。

  • 调用num时,调用堆栈为: test-> get_current_frame-> show_frame 。在后续调用中,堆栈为 get_current_frame ---> show_frame ,直到test()的循环为for中的range(4)完成为止。如果我们从顶部检查输出,则堆栈顶部的框架的深度为0: test(),而调用函数是lambda函数本身。行号frame = sys._getframe(0)中的74是当前行号。调用此函数的时间(将其想象为该帧的最后一个光标位置)。最后,我们看一下堆栈底部的框架。这也是日志记录中使用的框架对象(深度为3):

    file/line = main.py:74

    在日志记录中,我们需要3的深度才能到达调用程序函数的堆栈框架。

    我们还可以使用我们先前的玩具示例来理解这个概念。由于堆栈不太深,因此我们将当前帧设为 depth 0。

    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
    

    现在,如果我的Python实现没有import sys frameobj = sys._getframe() print(frameobj.f_code.co_name) #Output: <module> 的{​​{1}}属性怎么办?在这种情况下,_getframe()中的代码将执行并引发异常,以从sys获取当前帧。以下函数执行此操作,此处的调用函数再次为else(注意输出):

    traceback

    <module>返回当前Exception返回的回溯帧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 的帧对象。我们可以通过打印return语句f_back进行检查,然后得到类似tb_frame

    的信息。

    这说明了日志记录模块如何捕获当前帧。

    那么,以后在日志记录源代码中使用print(sys.exc_info()[2].tb_frame.f_back)是什么?您会在这里找到它:

    <frame object at 0x000000000049B2C8>

    上面的函数获取调用者函数的当前框架,并在以后使用此信息来获取与我们先前访问的属性相同的属性(文件名等)。

    答案 1 :(得分:1)

    很明显-在出现某些异常之前,没有exc_info(异常信息)。因此,我们需要引发异常来访问其信息并从中获取调用堆栈。

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