【问题标题】:Why is the source code of the Python logging module raising an exception in try?为什么 Python logging 模块的源代码在 try 中会引发异常?
【发布时间】:2019-06-16 23:57:21
【问题描述】:

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

【问题讨论】:

    标签: python exception logging


    【解决方案1】:

    根据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__.pyif-else 语句中的相同代码。唯一的区别是我们将depth 参数x 传递给lambda 函数用来在该depth 处抓取框架对象的函数:@ 987654346@.

    • Function show_frame(num, frame):这个函数prints frame 对象,带有depth的frame函数调用, sys._getframe(num),调用者函数名,例如。 show_frame()..等等。 , 执行调用函数代码的文件的文件名以及当前行号。在调用函数的代码中。 f_codesys._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 获取当前帧。以下函数执行此操作,此处的调用函数再次为&lt;module&gt;(注意输出):

    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),我们会得到类似:&lt;frame object at 0x000000000049B2C8&gt;

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

    那么,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---->
    

    上述函数获取调用者函数的当前帧,稍后使用此信息获取我们之前访问的相同属性(文件名等)。

    【讨论】:

    • 感谢您的出色回答。我得到了你的话。 sys._getframe() 和 sys.exc_info() 有什么区别。我们能否从 sys._getframe() 获得足够的信息?
    • 我刚刚在我的回答中添加了更多细节。
    【解决方案2】:

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

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

    【讨论】:

    • 简短而有用。我可以更加小心。
    猜你喜欢
    • 1970-01-01
    • 2023-03-31
    • 2020-10-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-02
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多