来自呼叫者的警告';s透视图(又称Python,相当于Perl的carp)?

来自呼叫者的警告';s透视图(又称Python,相当于Perl的carp)?,python,stack-trace,callstack,Python,Stack Trace,Callstack,简短版本: 有没有办法在Python中实现与Perl的实用程序相同的效果 长版本(适用于不熟悉Carp::Carp)的用户: 假设我们正在实现一些库API函数(即,它将被其他程序员在其代码中使用),比如说spam,并假设spam包含一些代码来检查传递给它的参数的有效性。当然,如果检测到这些参数存在任何问题,则该代码应该会引发异常。假设我们希望使相关的错误消息和回溯尽可能有助于调试某些客户机代码的人员 理想情况下,由该引发的异常生成的回溯的最后一行应该指出“有问题的代码”,即客户端代码中使用无效参

简短版本:

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

长版本(适用于不熟悉Carp::Carp)的用户:

假设我们正在实现一些库API函数(即,它将被其他程序员在其代码中使用),比如说
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
回溯(最近一次呼叫最后一次):
文件“client.py”,第3行,在
x=垃圾邮件(假,假)
文件“/home/jones/spam.py”,第7行,在spam中
_验证\u垃圾邮件\u参数(火腿、鸡蛋)
文件“/home/jones/spam.py”,第12行,在\u validate\u spam\u args中
提高价值错误('如果我们有火腿'
ValueError:如果我们有火腿,我们可以有火腿和鸡蛋(如果我们有鸡蛋)
而我们想要的将更接近:

% 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)
%python client.py
回溯(最近一次呼叫最后一次):
文件“client.py”,第3行,在
x=垃圾邮件(假,假)
ValueError:如果我们有火腿,我们可以有火腿和鸡蛋(如果我们有鸡蛋)
…将违规代码(
x=spam(False,False)
)作为回溯的最后一行

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

编辑:只是想澄清一下,这个问题不是关于LBYL vs EAFP,也不是关于先决条件或契约编程。如果我给了这个错误的印象,我很抱歉。这个问题是关于如何从调用堆栈的几个(一、两个)级别开始生成回溯


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

您可以使用
尝试..除了顶级API函数(
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

这实际上只是一个惯例问题,python中的异常处理被大量使用(请求原谅而不是请求许可).鉴于您在不同的语言环境中工作,您希望遵循这些约定-即/您确实希望让开发人员知道异常站点的位置。但是如果您确实需要这样做

使用检查模块

重建一个好的carp版本所需的一切都可以用它来完成,而不必担心装饰程序(见下文)。根据,这种方法可能会在python而不是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
msg at foo.pl line 6
    main::f() called at foo.pl line 10
    main::g() called at foo.pl line 13
使用回溯

这是最初提出的方法

通过操纵回溯对象,也可以用python编写与carp等价的代码,请参见中的文档。执行此操作的主要挑战是注入异常和回溯打印代码。值得注意的是,本节中的代码非常脆弱

# 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 main.py line 4
        __main__.f called at main.py line 7
        __main__.g called at main.py line 9
msg at foo.pl line 6
    main::f() called at foo.pl line 10
    main::g() called at foo.pl line 13

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

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

@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.
     ...

“从调用方的角度来看”,回溯仍然不会出现。我对回溯模块有点熟悉,但不知道如何将其用于我想做的事情。如果您有具体的想法,请帮助我查看一些代码草图。(另外,有关
$Carp::Carp
如何解决此问题的更多信息,请参阅我的最新编辑。)@kjo我有edi