2011-11-26 18 views
6

versión corta:Advertencias desde la perspectiva del llamador (también conocido como Python-equivalente de la carpa de Perl)?

¿Hay manera de lograr en Python el mismo efecto que se consigue por la utilidad de Perl Carp::carp?

Versión larga (para los que no conocen Carp::carp):

Supongamos que estamos llevando a cabo alguna función API de biblioteca (es decir, que está destinado a ser utilizado por otros programadores en su código), dicen spam, y supongamos que spam incluye algún código para verificar la validez de los argumentos que se le pasan. Por supuesto, se supone que este código genera una excepción si se detecta algún problema con estos argumentos. Digamos que queremos que el mensaje de error asociado y el rastreo sean lo más útiles posible para alguien que esté depurando algún código de cliente.

Idealmente, la última línea de la traza producida por este levantaron excepción debe señalar el "código erróneo", es decir, la línea en el código cliente donde spam fue llamado con argumentos no válidos.

Desafortunadamente, esto no es lo que sucedería, al menos por defecto, usando Python. En cambio, la última línea del traceback se referirá a algún lugar en el interior del código de la biblioteca, donde la excepción fue realmente raise 'd, lo que sería bastante oscuro para el público previsto de este traceback particular.

Ejemplo:

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

Cuando corremos client.py, obtenemos:

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

mientras que lo que queremos estaría más cerca de:

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

... con el infractor código (x = spam(False, False)) como la última línea del traceback.

Lo que necesitamos es una forma de informar el error "desde la perspectiva de la persona que llama" (que es lo que Carp::carp deja hacer en Perl).

EDITAR: Para que quede claro, esta pregunta no es sobre LBYL vs EAFP, ni sobre precondiciones o programación por contrato. Lo siento si di esta impresión equivocada. Esta pregunta se trata de cómo generar un rastreo desde unos pocos (uno, dos) niveles en la pila de llamadas.

Edit2: módulo de Python traceback es un lugar obvio para buscar un pitón-equivalente de Perl de Carp::carp, pero después de estudiar durante algún tiempo yo no era capaz de encontrar alguna manera de utilizarlo para lo que yo quiero hacer. FWIW, Carp::carp de Perl permite un ajuste fino de la trama inicial para la trazabilidad al exponer la variable global (por lo tanto de alcance dinámico) $Carp::CarpLevel. Las funciones de biblioteca que no son API que pueden -out, local -ize e incrementan esta variable en la entrada (por ejemplo, local $Carp::CarpLevel += 1;). No veo nada ni remotamente como este módulo de Python traceback. Entonces, a menos que me haya perdido algo, cualquier solución que use Python's traceback tendría que tomar un rumbo diferente ...

Respuesta

2

Esto es realmente solo una cuestión de convención, el manejo de excepciones en python está diseñado para ser utilizado en gran medida (pedir perdón en lugar de pedir permiso). Y dado que estás trabajando en un espacio de lenguaje diferente, quieres seguir esas convenciones, es decir,/realmente quieres que los desarrolladores sepan dónde estaba el sitio de la excepción. Pero si tu realmente no necesita hacer esto ...

Uso del módulo Inspeccionar

El inspect module va a hacer casi todo lo que necesita para reconstruir una versión nueva de la carpa, que funciona sin tener que preocuparse decoradores (ver a continuación). De acuerdo con la comments in this answer, puede ser que este enfoque se romperá en pitones que no sean 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) 

Lo que para el siguiente ejemplo:

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

produce la salida:

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

Uso de Traceback

Este fue el enfoque original propuesto.

Un equivalente de carpa también se podría escribir en python mediante la manipulación de objetos de rastreo, consulte la documentación en el traceback module. El principal desafío al hacer esto es inyectar el código de impresión de excepción y rastreo. Vale la pena señalar que el código en esta sección es muy frágil.

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

que puede ser llamada utilizando el siguiente proceso básico:

import carp 

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

def f(): 
    g() 

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

main() 

La salida de los cuales es:

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 Ejemplo de Referencia

Para completar, tanto Las soluciones propuestas en esta respuesta fueron debugg ed mediante la comparación de los resultados de esta equivalentes ejemplo 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(); 

que tiene la salida:

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

estoy un poco familiarizado con el módulo de rastreo, pero no veo ninguna manera de usarlo por lo que quiero hacer Si tiene algo concreto en mente, me sería útil ver algún bosquejo de código. (También, vea mi último EDIT, para más información sobre cómo '$ Carp :: carpa' resuelve este problema.) – kjo

+1

@kjo He editado mi respuesta para mostrar un enfoque posible use el módulo de rastreo –

+1

@kjo, he agregado una segunda solución que permite usar un python equivalente a carpa sin ningún código de soporte. –

1

Usted podría utilizar try..except en la función API de nivel superior (foo) para elevar una excepción diferente:

class FooError(Exception): pass 

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

def bar(): 
    baz() 

def baz(): 
    1/0 

foo() 

Por lo tanto, cuando un usuario de la API llama foo y se produce una excepción, todo lo que ven es el FooError y no el interno ZeroDivisionError.

+1

El rastreo todavía no aparecería "desde la perspectiva de la persona que llama" – kjo

1

Lo que se quiere hacer es conocida como el establecimiento de la función preconditions, y no hay soporte de idiomas para ello en Pitón. Python tampoco es tan hackeable como perl (a menos que tal vez si está utilizando PyPy) por lo que no se puede agregar de una manera totalmente transparente.

Dicho esto, el módulo PyContracts parece hacerlo de forma relativamente fluida utilizando decodificadores de función y especificaciones de precondición basadas en cadenas. No he usado el módulo yo mismo, pero parece que puede conseguirte algo más cerca de lo que quieres. Aquí está el primer ejemplo dado en su página de información:

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