从调用者的角度发出警告(又称Python,相当于Perl的鲤鱼)? [英] Warnings from caller's perspective (aka Python-equivalent of Perl's carp)?

查看:70
本文介绍了从调用者的角度发出警告(又称Python,相当于Perl的鲤鱼)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

简短版本:


是否可以用Python实现与Perl的 Carp :: carp 实用程序?


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



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



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



不幸的是,至少在默认情况下,使用Python不会发生这种情况。相反,回溯的最后一行将引用库代码内部的某个地方,该异常实际上是 raise 'd,这对于预期的目标而言是相当模糊的

示例:

 #spam.py(库代码)
def垃圾邮件(火腿,鸡蛋):
'''
用火腿和鸡蛋做一些愚蠢的事情。

火腿和鸡蛋中的至少一种必须为True。
'''
_validate_spam_args(火腿,鸡蛋)
返回火腿==鸡蛋

def _validate_spam_args(火腿,鸡蛋):
如果不是(火腿或鸡蛋):
提高ValueError('如果我们有火腿'
'我们可以有火腿和鸡蛋'
'(如果我们有鸡蛋)')



#client.py(客户端代码)
从垃圾邮件导入垃圾邮件

x =垃圾邮件(False,False)

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

 %python client.py 
回溯(最近一次调用):
文件 client.py,第3行,在< module>中;
x =垃圾邮件(False,False)
文件 /home/jones/spam.py,第7行,在垃圾邮件中
_validate_spam_args(火腿,鸡蛋)
文件 / home /jones/spam.py,第12行,在_validate_spam_args
中,引发ValueError('如果我们有火腿'
ValueError:如果我们有火腿,我们可以有火腿和鸡蛋(如果我们有鸡蛋)

而我们想要的东西更接近:

 %python client.py 
追溯(最近一次调用):
文件 client.py,第3行,在< module>
x中=垃圾邮件(False,False)
ValueError:如果我们有火腿,我们可以有火腿和鸡蛋(如果我们有鸡蛋)

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



我们需要的是一种从呼叫者的角度报告错误的方法(这就是 Carp :: carp



编辑:要清楚,这个问题不是关于LBYL vs E AFP,也不涉及前提条件或按合同编程。如果给我这个错误的印象,我感到抱歉。这个问题是关于如何从调用堆栈的几个(一,两个)级别开始产生回溯。



EDIT2:Python的 traceback 模块显然是寻找与Perl的 Carp :: carp 等效的Python的地方,但是研究了一段时间之后,我无法找到任何方法将其用于我想做的事。 FWIW,Perl的 Carp :: carp 允许通过暴露全局变量(因此具有动态作用域) $ Carp来微调回溯的初始帧: :CarpLevel 。非API库函数可能会鲤鱼 -out,本地 -ize并在输入时增加此变量(例如, local $ Carp :: CarpLevel + = 1; )。我没有看到甚至是远程这样的Python的 traceback 模块。因此,除非我错过了任何事情,否则任何使用Python的 traceback 的解决方案都必须采取一种截然不同的方法...

解决方案

这实际上只是一个约定问题,python中的异常处理被设计为大量使用(乞求宽恕而不是请求许可)。考虑到您使用的语言空间不同,您想要遵循这些约定-即,您确实想让开发人员知道例外站点在哪里。但是如果您确实需要执行此操作...



使用检查模块



检查模块几乎可以完成重建一个不错的版本所需的一切鲤鱼,它的工作而不必担心装饰器(见下文)。根据此答案中的注释,可能是

 #修改后的carp.py 
import sys
进口检查

def carp(msg):
#抓取当前调用堆栈,并删除我们不想要的东西
stack = inspect.stack()
stack = stack [1:]

caller_func = stack [0] [1]
caller_line = stack [0] [2]
sys.stderr.write('%s at %s行%d\n'%(消息,caller_func,caller_line))

for idx,枚举中的帧(堆栈[1:]):
#帧,向上从`frame`
upframe = stack [idx]
upframe_record = upframe [0]
upframe_func = upframe [3]
upframe_module = inspect.getmodule(upframe_record).__ name__

#当前帧中我们需要的东西
frame_file = frame [1 ]
frame_line = frame [2]

sys.stderr.write('\t%s。%s'%(upframe_module,upframe_func))
sys.stderr。 write('在%s行%d\n'%(frame_file,frame_line)处调用)

#退出,规避(大多数)异常处理
sys.exit(1)

以下示例:

  1进口鲤鱼
2
3 def f():
4 carp.carp('carpmsg')
5
6 def g ():
7 f()
8
9 g()

产生输出:

  msg在main.py第4行
__main __。f在main.py处调用第7行
__main __。g在main.py第9行被调用

使用回溯



这是最初提出的方法。



等效鲤鱼也可以通过操纵回溯对象用python编写,请参阅t中的文档他回溯模块。这样做的主要挑战是注入异常和回溯打印代码。值得注意的是,本节中的代码非常脆弱。

 #carp.py 
import sys
import traceback

'''
carp.py-Perls概念的部分仿真Carp :: carp
'''

类CarpError(Exception):
def __init __(self,value):
self.value =值
def __str __(self):
return repr(self.value)

def carpmain(fun):
def impl():
try:
fun()
除了CarpError如:
_,_, tb = sys.exc_info()
项目= traceback.extract_tb(tb)[:-1]
文件名,lineno,funcname,line =项目[-1]
打印'%s at %s行%d'%(例如值,文件名,行号)item [1:]中的

文件名,lineno,funcname,行=项目
打印'\ t%s在%s行%d'%(函数名,文件名,行号)处调用
return impl

def carp(v alue):
提高CarpError(value)

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

 进口鲤鱼

def g():
carp.carp('pmsg ')

def f():
g()

@ carp.carpmain
def main():
f()

main()

其输出为:

  msg在foo.py第4行
主要在foo.py第12行调用
f在foo.py第7行$ b在foo.py第4行调用$ bg

Perl参考示例

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

  1使用严格; 
2使用警告;
3次使用鲤鱼;
4
5 sub f {
6 Carp :: carp( msg);
7}
8
9 sub g {
10 f();
11}
12
13 g();

输出如下:

  msg在foo.pl第6行
main :: f()在foo.pl第10行调用
main :: g()在foo.pl第13行调用b $ b


Short version:

Is there way to achieve in Python the same effect achieved by Perl's Carp::carp utility?

Long version (for those unfamiliar with Carp::carp):

Suppose we are implementing some library API function (i.e., it is meant to be used by other programmers in their code), say spam, and suppose that spam includes some code to check the validity of the arguments passed to it. Of course, this code is supposed to raise an exception if any problem with these arguments is detected. Let's say that we want to make the associated error message and traceback as helpful as possible to someone debugging some client code.

Ideally, the last line of the traceback produced by this raised exception should pinpoint the "offending code", namely the line in the client code where spam was called with invalid arguments.

Unfortunately, this is not what would happen, at least by default, using Python. Instead, the last line of the traceback will refer to somewhere in the internals of the library code, where the exception was actually raise'd, which would be quite obscure to the intended audience of this particular traceback.

Example:

# 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)

When we run client.py, we get:

% 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)

whereas what we want would be closer to:

% 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)

...with the offending code (x = spam(False, False)) as the last line of the traceback.

What we need is some way to report the error "from the perspective of the caller" (which is what Carp::carp lets one do in Perl).

EDIT: Just to be clear, this question is not about LBYL vs EAFP, nor about preconditions or programming-by-contract. I am sorry if I gave this wrong impression. This question is about how to produce a traceback starting from a few (one, two) levels up the call stack.

EDIT2: Python's traceback module is an obvious place to look for a Python-equivalent of Perl's Carp::carp, but after studying it for some time I was not able to find any way to use it for what I want to do. FWIW, Perl's Carp::carp allows fine-adjusting of the initial frame for the traceback by exposing the global (hence dynamically scoped) variable $Carp::CarpLevel. Non-API library functions that may carp-out, local-ize and increase this variable on entry (e.g. local $Carp::CarpLevel += 1;). I don't see anything even remotely like this Python's traceback module. So, unless I missed something, any solution that uses Python's traceback would have to take a rather different tack...

解决方案

This is really just a matter of convention, exception handling in python is designed to be used heavily (beg forgiveness rather than ask permission). And given that you're working in a different language space, you want to follow those conventions - ie/ you really do want to let developers know where the site of the exception was. But if your really do need to do this ...

Using the Inspect Module

The inspect module will do pretty much everything you need to reconstruct a nice version of carp, that works without having to worry about decorators (see below). As per the comments in this answer, it may be that this approach will break on pythons other than 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)

Which for the following example:

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

Produces the output:

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

Using Traceback

This was the original approach proposed.

An equivalent to carp could also be written in python by manipulating traceback objects, see the documentation in the traceback module. The main challenge in doing this turns out to be injecting the exception and traceback print code. It is worth noting that the code in this section is very fragile.

# 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 )

Which can be called using the following basic process:

import carp

def g():
    carp.carp( 'pmsg' )

def f():
    g()

@carp.carpmain
def main():
    f()

main()

The output of which is:

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 Reference Example

For completeness, both solutions proposed in this answer were debugged by comparing results to this equivalent perl example:

  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();

Which has the output:

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

这篇关于从调用者的角度发出警告(又称Python,相当于Perl的鲤鱼)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆