【问题标题】:Warnings from caller's perspective (aka Python-equivalent of Perl's carp)?从调用者的角度发出的警告(又名 Python 相当于 Perl 的鲤鱼)?
【发布时间】:2012-01-06 17:25:07
【问题描述】:

短版:

有没有办法在 Python 中实现与 Perl 的 @987654321@ 实用程序相同的效果?

长版(对于不熟悉Carp::carp的人):

假设我们正在实现一些库 API 函数(即,它旨在供其他程序员在他们的代码中使用),比如spam,并假设spam 包含一些代码检查传递给它的参数的有效性。当然,如果检测到这些参数有任何问题,这段代码应该引发异常。假设我们想让相关的错误消息和回溯对调试某些客户端代码的人尽可能有用。

理想情况下,此引发的异常产生的回溯的最后一行应查明“违规代码”,即客户端代码中使用无效参数调用spam 的行。 p>

不幸的是,至少在默认情况下,使用 Python 不会发生这种情况。相反,回溯的最后一行将引用库代码内部的某个地方,其中异常实际上是 raise'd,这对于 this 特定的目标受众来说是非常模糊的追溯。

例子:

# spam.py (library code)
def spam(ham, eggs):
    '''
    Do something stupid with ham and eggs.

    At least one of ham and eggs must be True.
    '''
    _validate_spam_args(ham, eggs)
    return ham == eggs

def _validate_spam_args(ham, eggs):
    if not (ham or eggs):
        raise ValueError('if we had ham '
                         'we could have ham and eggs '
                         '(if we had eggs)')



# client.py (client code)
from spam import spam

x = spam(False, False)

当我们运行 client.py 时,我们得到:

% python client.py
Traceback (most recent call last):
  File "client.py", line 3, in <module>
    x = spam(False, False)
  File "/home/jones/spam.py", line 7, in spam
    _validate_spam_args(ham, eggs)
  File "/home/jones/spam.py", line 12, in _validate_spam_args
    raise ValueError('if we had ham '
ValueError: if we had ham we could have ham and eggs (if we had eggs)

而我们想要的更接近:

% python client.py
Traceback (most recent call last):
  File "client.py", line 3, in <module>
    x = spam(False, False)
ValueError: if we had ham we could have ham and eggs (if we had eggs)

...将违规代码 (x = spam(False, False)) 作为回溯的最后一行。

我们需要的是某种“从调用者的角度”报告错误的方法(这是Carp::carp 允许人们在 Perl 中做的事情)。

编辑:为了清楚起见,这个问题不是关于 LBYL 与 EAFP,也不是关于先决条件或按合同编程。如果我给了这个错误的印象,我很抱歉。这个问题是关于如何从调用堆栈的几个(一、二)级开始产生回溯。

EDIT2:Python 的 traceback 模块显然是寻找与 Perl 的 Carp::carp 等效的 Python 的地方,但在研究了一段时间后,我无法找到任何方法将它用于我想要的做。 FWIW,Perl 的Carp::carp 允许通过公开全局(因此动态范围)变量$Carp::CarpLevel 来微调回溯的初始帧。可能carp-out、local-ize 并在输入时增加此变量的非 API 库函数(例如 local $Carp::CarpLevel += 1;)。我没有看到任何类似 Python 的 traceback 模块的东西甚至是远程。因此,除非我错过了什么,否则任何使用 Python 的 traceback 的解决方案都必须采取完全不同的策略......

【问题讨论】:

    标签: python stack-trace callstack


    【解决方案1】:

    这实际上只是一个约定问题,python 中的异常处理旨在大量使用(请求宽恕而不是请求许可)。鉴于您在不同的语言空间中工作,您希望遵循这些约定 - 即/您确实希望让开发人员知道异常站点在哪里。但如果你真的需要这样做......

    使用检查模块

    inspect module 几乎可以完成重建漂亮版本 carp 所需的一切工作,无需担心装饰器(见下文)。根据comments in this answer,这种方法可能会在 cpython 以外的其他 python 上中断

    # revised carp.py
    import sys
    import inspect
    
    def carp( msg ):
        # grab the current call stack, and remove the stuff we don't want
        stack = inspect.stack()
        stack = stack[1:]
    
        caller_func = stack[0][1]
        caller_line = stack[0][2]
        sys.stderr.write('%s at %s line %d\n' % (msg, caller_func, caller_line))
    
        for idx, frame in enumerate(stack[1:]):
            # The frame, one up from `frame`
            upframe = stack[idx]
            upframe_record = upframe[0]
            upframe_func   = upframe[3]
            upframe_module = inspect.getmodule(upframe_record).__name__
    
            # The stuff we need from the current frame
            frame_file = frame[1]
            frame_line = frame[2]
    
            sys.stderr.write( '\t%s.%s ' % (upframe_module, upframe_func) )
            sys.stderr.write( 'called at %s line %d\n' % (frame_file, frame_line) )
    
        # Exit, circumventing (most) exception handling
        sys.exit(1)
    

    以下示例:

      1 import carp
      2
      3 def f():
      4     carp.carp( 'carpmsg' )
      5
      6 def g():
      7     f()
      8
      9 g()
    

    产生输出:

    msg at main.py line 4
            __main__.f called at main.py line 7
            __main__.g called at main.py line 9
    

    使用回溯

    这是最初提出的方法。

    也可以通过操纵回溯对象在 python 中编写与 carp 等效的内容,请参阅traceback module 中的文档。这样做的主要挑战是注入异常和回溯打印代码。值得注意的是,这部分的代码非常脆弱。

    # carp.py
    import sys
    import traceback
    
    '''
    carp.py - partial emulation of the concept of perls Carp::carp
    '''
    
    class CarpError(Exception):
        def __init__(self, value):
            self.value = value
        def __str__(self):
            return repr(self.value)
    
    def carpmain( fun ):
        def impl():
            try:
                fun()
            except CarpError as ex:
                _, _, tb = sys.exc_info()
                items = traceback.extract_tb(tb)[:-1]
                filename, lineno, funcname, line = items[-1]
                print '%s at %s line %d' % (ex.value, filename, lineno)
                for item in items[1:]:
                    filename, lineno, funcname, line = item
                    print '\t%s called at %s line %d' % (funcname, filename, lineno)
        return impl
    
    def carp( value ):
        raise CarpError( value )
    

    可以使用以下基本流程调用:

    import carp
    
    def g():
        carp.carp( 'pmsg' )
    
    def f():
        g()
    
    @carp.carpmain
    def main():
        f()
    
    main()
    

    其输出为:

    msg at foo.py line 4
        main called at foo.py line 12
        f called at foo.py line 7
        g called at foo.py line 4
    

    Perl 参考示例

    为了完整起见,此答案中提出的两种解决方案都是通过将结果与等效的 perl 示例进行比较来调试的:

      1 use strict;
      2 use warnings;
      3 use Carp;
      4
      5 sub f {
      6     Carp::carp("msg");
      7 }
      8
      9 sub g {
     10     f();
     11 }
     12
     13 g();
    

    哪个有输出:

    msg at foo.pl line 6
        main::f() called at foo.pl line 10
        main::g() called at foo.pl line 13
    

    【讨论】:

    • 我对 traceback 模块有点熟悉,但看不到任何方法可以将它用于我想做的事情。如果您有一些具体的想法,那将有助于我查看它的一些代码草图。 (另外,请参阅我的最新编辑,了解更多关于 $Carp::carp 如何解决此问题的信息。)
    • @kjo 我已经编辑了我的答案,以展示使用回溯模块的一种可能方法
    • @kjo,我添加了第二个解决方案,它允许在没有任何支持代码的情况下使用相当于 carp 的 python。
    【解决方案2】:

    您可以在顶级 API 函数 (foo) 中使用 try..except 来引发不同的异常:

    class FooError(Exception): pass
    
    def foo():
        try:
            bar()
        except ZeroDivisionError:
            raise FooError()
    
    def bar():
        baz()
    
    def baz():
        1/0
    
    foo()
    

    因此,当 API 用户调用 foo 并引发异常时,他们看到的只是 FooError 而不是内部的 ZeroDivisionError

    【讨论】:

    • “从调用者的角度来看”回溯仍然不会出现
    【解决方案3】:

    您要做的是建立函数preconditions,Python 中没有语言支持。 Python 也不像 perl 那样完全可破解(除非您使用 PyPy),因此无法以完全无缝的方式添加它。

    话虽如此,模块PyContracts 似乎使用函数装饰器和基于字符串的前置条件规范相对顺利地完成了这项工作。我自己没有使用过这个模块,但它看起来确实可以让你更接近你想要的东西。这是其信息页面上给出的第一个示例:

    @contract
    def my_function(a : 'int,>0', b : 'list[N],N>0') -> 'list[N]':
         # Requires b to be a nonempty list, and the return
         # value to have the same length.
         ...
    

    【讨论】:

      猜你喜欢
      • 2018-06-03
      • 2010-10-03
      • 2020-04-29
      • 2013-08-15
      • 2011-04-01
      • 2018-11-16
      • 1970-01-01
      • 1970-01-01
      • 2011-02-18
      相关资源
      最近更新 更多