Предупреждения с точки зрения вызывающего (он же Python-эквивалент карпа Perl)? - PullRequest
7 голосов
/ 26 ноября 2011

Короткая версия:

Есть ли способ достичь в Python того же эффекта, который достигается утилитой Perl <a href="http://perldoc.perl.org/Carp.html">Carp::carp</a>?

Длинная версия (для тех, кто не знаком с 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 является очевидным местом для поиска Python-эквивалента Carp::carp в Perl, но после изучения его в течение некоторого времени я не смог найти какой-либо способ использовать его для того, что я хочу делать. FWIW, Perl Carp::carp позволяет точно настроить начальный кадр для трассировки путем предоставления глобальной (и, следовательно, динамической области) переменной $Carp::CarpLevel. Библиотечные функции без API, которые могут carp -out, local -ize и увеличивать эту переменную при входе (например, local $Carp::CarpLevel += 1;). Я не вижу ничего , даже удаленно , как у этого модуля Python traceback. Так что, если я что-то пропустил, любое решение, использующее Python traceback, должно было бы пойти по другому пути ...

Ответы [ 3 ]

2 голосов
/ 26 ноября 2011

Это на самом деле просто условность, обработка исключений в python предназначена для интенсивного использования (прошу прощения, а не спрашиваю разрешения). И учитывая, что вы работаете в другом языковом пространстве, вы хотите следовать этим соглашениям - т.е. / вы действительно хотите, чтобы разработчики знали, где был сайт исключения. Но если вам действительно нужно это сделать ...

Использование модуля проверки

Модуль проверки сделает почти все, что вам нужно для восстановления хорошей версии carp, которая работает, не беспокоясь о декораторах (см. Ниже). Согласно комментариям в этом ответе , возможно, что этот подход сломается на питонах, отличных от 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

Использование трассировки

Это был первоначальный предложенный подход.

Эквивалент carp также может быть написан на python путем манипулирования объектами трассировки, см. Документацию в модуле traceback . Основной проблемой при этом оказывается внедрение кода исключения и трассировки печати. Стоит отметить, что код в этом разделе очень хрупкий.

# 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 голос
/ 26 ноября 2011

То, что вы хотите сделать, известно как установление функции предварительные условия , и в 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.
     ...
0 голосов
/ 26 ноября 2011

Вы можете использовать try..except в функции 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.

...