从调用者的角度来看警告(又名Python等同于Perl的鲤鱼)?

时间:2011-11-26 01:57:44

标签: python stack-trace callstack

简短版本:

  

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

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

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

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

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

示例:

# 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的非API库函数 - 在条目上增加和增加此变量(例如local $Carp::CarpLevel += 1;)。我没有看到任何甚至远程这样的Python的traceback模块。因此,除非我遗漏了某些内容,否则任何使用Python traceback的解决方案都必须采用相当不同的方法......

3 个答案:

答案 0 :(得分:2)

这实际上只是一个惯例,python中的异常处理被设计为大量使用(请求宽恕而不是请求权限)。鉴于您在不同的语言空间工作,您希望遵循这些约定 - 即/您确实希望让开发人员知道例外站点的位置。但如果你真的需要这样做......

使用Inspect模块

inspect module几乎可以完成重建好的鲤鱼版本所需的一切,无需担心装饰器(见下文)。根据{{​​3}},这种方法可能会在cpython以外的蟒蛇上破坏

# 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

使用跟踪

这是提出的原始方法。

也可以通过操纵traceback对象在python中编写等效的carp,参见comments in this answer中的文档。这样做的主要挑战是注入异常和回溯打印代码。值得注意的是,本节中的代码非常脆弱。

# 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

答案 1 :(得分:1)

您想要做的事情被称为建立函数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.
     ...

答案 2 :(得分:0)

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

class FooError(Exception): pass

def foo():
    try:
        bar()
    except ZeroDivisionError:
        raise FooError()

def bar():
    baz()

def baz():
    1/0

foo()

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