¿Cuál es la forma Pythonic de reportar errores no fatales en un analizador?

Un analizador que creé lee juegos de ajedrez grabados de un archivo. La API se utiliza de esta manera:

import chess.pgn pgn_file = open("games.pgn") first_game = chess.pgn.read_game(pgn_file) second_game = chess.pgn.read_game(pgn_file) # ... 

A veces se encuentran movimientos ilegales (u otros problemas). ¿Cuál es una buena manera pythonica de manejarlos?

  • Generando excepciones tan pronto como se encuentre el error. Sin embargo, esto hace que todos los problemas sean fatales, ya que la ejecución se detiene. A menudo, todavía hay datos útiles que se han analizado y podrían devolverse. Además, no puede simplemente continuar analizando el siguiente conjunto de datos, porque todavía estamos en medio de algunos datos de media lectura.

  • Acumulando excepciones y elevándolas al final del juego. Esto hace que el error vuelva a ser fatal, pero al menos puedes atraparlo y continuar analizando el siguiente juego.

  • Introduce un argumento opcional como este:

       game = chess.pgn.read_game(pgn_file, parser_info) if parser_info.error: # This appears to be quite verbose. # Now you can at least make the best of the sucessfully parsed parts. # ... 

    ¿Se utilizan algunos de estos u otros métodos en la naturaleza?

    En realidad, esos son errores fatales, al menos en lo que respecta a poder reproducir un juego correcto; por otro lado, tal vez el jugador realmente hizo el movimiento ilegal y nadie se dio cuenta en ese momento (lo que lo convertiría en una advertencia, no en un error fatal).

    Dada la posibilidad de errores fatales (archivo dañado) y advertencias (se hizo un movimiento ilegal, pero los movimientos subsiguientes muestran consistencia con ese movimiento (en otras palabras, error del usuario y nadie lo detectó en ese momento)) Recomiendo una combinación de La primera y segunda opciones:

    • generar una excepción cuando el análisis continuo no es una opción
    • recostack cualquier error / advertencia que no impida más análisis hasta el final

    Si no encuentra un error fatal, puede devolver el juego, además de cualquier advertencia / error no fatal, al final:

     return game, warnings, errors 

    Pero ¿y si haces un error fatal?

    No hay problema: cree una excepción personalizada a la que puede adjuntar la parte utilizable del juego y cualquier otra advertencia / error no fatal a:

     raise ParsingError( 'error explanation here', game=game, warnings=warnings, errors=errors, ) 

    luego, cuando detecte el error, podrá acceder a la parte recuperable del juego, junto con las advertencias y los errores.

    El error personalizado podría ser:

     class ParsingError(Exception): def __init__(self, msg, game, warnings, errors): super().__init__(msg) self.game = game self.warnings = warnings self.errors = errors 

    y en uso:

     try: first_game, warnings, errors = chess.pgn.read_game(pgn_file) except chess.pgn.ParsingError as err: first_game = err.game warnings = err.warnings errors = err.errors # whatever else you want to do to handle the exception 

    Esto es similar a cómo el módulo de subprocess maneja los errores.

    Para la capacidad de recuperar y analizar juegos subsiguientes después de un error fatal del juego, sugeriría un cambio en su API:

    • tener un iterador de juego que simplemente devuelve los datos en bruto para cada juego (solo tiene que saber cómo saber cuándo termina un juego y comienza el siguiente)
    • haz que el analizador tome los datos sin procesar del juego y los analice (por lo que ya no está a cargo de dónde estás en el archivo)

    De esta manera, si tienes un archivo de cinco juegos y dos juegos, puedes intentar analizar los juegos 3, 4 y 5.

    La forma más Pythonic es el módulo de registro . Se ha mencionado en los comentarios, pero lamentablemente sin insistir lo suficiente. Hay muchas razones por las que es preferible a las advertencias :

    1. El módulo de advertencias está diseñado para informar advertencias sobre posibles problemas de código , no datos de usuario incorrectos.
    2. La primera razón es realmente suficiente. 🙂
    3. El módulo de registro proporciona una severidad de mensaje ajustable: no solo se pueden informar advertencias, sino que se puede informar cualquier cosa, desde mensajes de depuración hasta errores críticos.
    4. Puede controlar completamente la salida del módulo de registro. Los mensajes se pueden filtrar por su origen, contenido y gravedad, formateados de la forma que desee, enviados a diferentes destinos de salida (consola, tuberías, archivos, memoria, etc.) …
    5. El módulo de registro separa los informes y resultados reales de errores / advertencias / mensajes: su código puede generar mensajes del tipo apropiado y no tiene que preocuparse de cómo se presentan al usuario final.
    6. El módulo de registro es el estándar de facto para el código Python. Todos en todas partes lo están usando. Entonces, si su código lo está utilizando, combinarlo con un código de terceros (que también es probable que use el registro) será muy sencillo. Bueno, tal vez algo más fuerte que la brisa, pero definitivamente no es un huracán de categoría 5. 🙂

    Un caso de uso básico para el módulo de registro sería:

     import logging logger = logging.getLogger(__name__) # module-level logger # (tons of code) logger.warning('illegal move: %s in file %s', move, file_name) # (more tons of code) 

    Esto imprimirá mensajes como:

     WARNING:chess_parser:illegal move: a2-b7 in file parties.pgn 

    (asumiendo que su módulo se llama chess_parser.py)

    Lo más importante es que no necesita hacer nada más en su módulo de análisis . Declara que está utilizando el sistema de registro, está utilizando un registrador con un nombre específico (igual que el nombre de su módulo analizador en este ejemplo) y le está enviando mensajes de nivel de advertencia. Su módulo no tiene que saber cómo estos mensajes se procesan, formatean y reportan al usuario. O si se informan en absoluto. Por ejemplo, puede configurar el módulo de registro (generalmente al comienzo de su progtwig) para usar un formato diferente y volcarlo en un archivo:

     logging.basicConfig(filename = 'parser.log', format = '%(name)s [%(levelname)s] %(message)s') 

    Y de repente, sin ningún cambio en el código de su módulo, sus mensajes de advertencia se guardan en un archivo con un formato diferente en lugar de imprimirse en la pantalla:

     chess_parser [WARNING] illegal move: a2-b7 in file parties.pgn 

    O puede suprimir las advertencias si lo desea:

     logging.basicConfig(level = logging.ERROR) 

    Y las advertencias de su módulo se ignorarán completamente, mientras que cualquier mensaje de ERROR o de nivel superior de su módulo aún se procesará.

    Ofrecí la recompensa porque me gustaría saber si esta es realmente la mejor manera de hacerlo. Sin embargo, también estoy escribiendo un analizador y por eso necesito esta funcionalidad, y esto es lo que he encontrado.


    El módulo de warnings es exactamente lo que quieres.

    Lo que me apartó de ello al principio fue que cada ejemplo de advertencia que se usa en los documentos se parece a esto :

     Traceback (most recent call last): File "warnings_warn_raise.py", line 15, in  warnings.warn('This is a warning message') UserWarning: This is a warning message 

    … lo cual es indeseable porque no quiero que sea una UserWarning , quiero mi propio nombre de advertencia personalizado.

    Aquí está la solución a eso:

     import warnings class AmbiguousStatementWarning(Warning): pass def x(): warnings.warn("unable to parse statement syntax", AmbiguousStatementWarning, stacklevel=3) print("after warning") def x_caller(): x() x_caller() 

    lo que da:

     $ python3 warntest.py warntest.py:12: AmbiguousStatementWarning: unable to parse statement syntax x_caller() after warning 

    No estoy seguro de si la solución es pythonic o no, pero la uso con bastante frecuencia con pequeñas modificaciones: un analizador hace su trabajo dentro de un generador y produce resultados y un código de estado. El código de recepción toma decisiones sobre qué hacer con los elementos fallidos:

     def process_items(items) for item in items: try: #process item yield processed_item, None except StandardError, err: yield None, (SOME_ERROR_CODE, str(err), item) for processed, err in process_items(items): if err: # process and log err, collect failed items, etc. continue # further process processed 

    Un enfoque más general es practicar en el uso de patrones de diseño. Una versión simplificada de Observer (cuando registra devoluciones de llamadas para errores específicos) o un tipo de Visitante (donde el visitante tiene métodos para procesar errores específicos, consulte el analizador de SAX para obtener información) puede ser una solución clara y bien entendida.

    Sin las bibliotecas, es difícil hacerlo de manera limpia, pero aún así es posible.

    Existen diferentes métodos para manejar esto, dependiendo de la situación.

    Método 1:

    Ponga todos los contenidos de bucle while dentro de lo siguiente:

     while 1: try: #codecodecode except Exception as detail: print detail 

    Método 2:

    Igual que el Método 1, excepto que tiene varias opciones try / except, por lo que no omite mucho código y usted conoce la ubicación exacta del error.

    Lo siento, en un apuro, espero que esto ayude!