什么是Pythonic在解析器中报告非致命错误的方法?

Nik*_*las 9 python error-handling warnings exception-handling

我创建的解析器从文件中读取记录的国际象棋游戏.API使用如下:

import chess.pgn

pgn_file = open("games.pgn")

first_game = chess.pgn.read_game(pgn_file)
second_game = chess.pgn.read_game(pgn_file)
# ...
Run Code Online (Sandbox Code Playgroud)

有时会遇到非法移动(或其他问题).什么是好的Pythonic方式来处理它们?

  • 遇到错误时立即引发异常.但是,这会使每个问题都致命,因为执行会停止.通常,仍然有用的数据已被解析并可以返回.此外,您不能简单地继续解析下一个数据集,因为我们仍处于一些半读数据的中间.

  • 累积异常并在游戏结束时提升它们.这使得错误再次致命,但至少你可以抓住它并继续解析下一场比赛.

  • 引入一个这样的可选参数:

    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.
       # ...
    
    Run Code Online (Sandbox Code Playgroud)

这些或其他方法是否在野外使用?

Lav*_*Lav 10

最Pythonic方式是日志记录模块.在评论中已经提到过,但遗憾的是没有足够强调这一点.警告有多种原因:

  1. 警告模块旨在报告有关潜在代码问题的警告,而不是错误的用户数据.
  2. 第一个原因实际上就够了.:-)
  3. 记录模块提供可调整的消息严重性:不仅可以报警,还可以报告从调试消息到严重错误的任何内容.
  4. 您可以完全控制记录模块的输出.消息可以按其来源,内容和严重性进行过滤,以您希望的任何方式格式化,发送到不同的输出目标(控制台,管道,文件,内存等)......
  5. 记录模块将实际的错误/警告/消息报告和输出分开:您的代码可以生成适当类型的消息,而不必担心它们如何呈现给最终用户.
  6. 日志记录模块是Python代码的事实标准.世界各地的每个人都在使用它.因此,如果您的代码正在使用它,将它与第三方代码(可能也使用日志记录)相结合将是轻而易举的.好吧,也许比微风更强大,但绝对不是5级飓风.:-)

日志记录模块的基本用例如下所示:

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)
Run Code Online (Sandbox Code Playgroud)

这将打印如下消息:

WARNING:chess_parser:illegal move: a2-b7 in file parties.pgn
Run Code Online (Sandbox Code Playgroud)

(假设您的模块名为chess_parser.py)

最重要的是,您不需要在解析器模块中执行任何其他操作.您声明您正在使用日志系统,您正在使用具有特定名称的记录器(与此示例中的解析器模块名称相同),并且您正在向其发送警告级别消息.您的模块不必知道如何处理,格式化这些消息并将其报告给用户.或者如果他们被报道的话.例如,您可以配置日志记录模块(通常在程序的最开头)使用不同的格式并将其转储到文件中:

logging.basicConfig(filename = 'parser.log', format = '%(name)s [%(levelname)s] %(message)s')
Run Code Online (Sandbox Code Playgroud)

突然,在没有对模块代码进行任何更改的情况下,您的警告消息将保存到具有不同格式的文件中,而不是打印到屏幕上:

chess_parser [WARNING] illegal move: a2-b7 in file parties.pgn
Run Code Online (Sandbox Code Playgroud)

或者,如果您愿意,可以禁止警告:

logging.basicConfig(level = logging.ERROR)
Run Code Online (Sandbox Code Playgroud)

并且您的模块警告将被完全忽略,而模块中的任何ERROR或更高级别的消息仍将被处理.

  • @cat没有什么可以阻止你将回溯添加到日志消息中 - 这是格式化的问题(并且可能在`logging.warning()`调用中添加一个额外的参数).默认情况下,与日志记录相比,警告模块会执行更多操作,但日志记录可以完成所有这些操作,甚至不是竞争.我真的建议尝试记录 - 乍一看它看起来不张扬,但它非常强大. (3认同)

Eth*_*man 8

实际上,这些都是致命的错误 - 至少,只要能够重现正确的游戏; 另一方面,也许玩家确实做了非法移动,当时没有人注意到(这会使其成为警告,而不是致命的错误).

鉴于致命错误(文件已损坏)和警告(非法移动的可能性,但随后的移动显示与该移动的一致性(换句话说,用户错误,当时没有人抓住它))我建议组合第一个和第二个选项:

  • 在继续解析不是一个选项时引发异常
  • 收集任何错误/警告,不排除进一步解析直到结束

如果您没有遇到致命错误,那么您可以在最后返回游戏,加上任何警告/非致命错误:

return game, warnings, errors
Run Code Online (Sandbox Code Playgroud)

但是,如果你遇到了致命的错误怎么办?

没问题:创建一个自定义异常,您可以将游戏的可用部分和任何其他警告/非致命错误附加到:

raise ParsingError(
    'error explanation here',
    game=game,
    warnings=warnings,
    errors=errors,
    )
Run Code Online (Sandbox Code Playgroud)

然后当你发现错误时,你可以访问游戏的可恢复部分,以及警告和错误.

自定义错误可能是:

class ParsingError(Exception):
    def __init__(self, msg, game, warnings, errors):
        super().__init__(msg)
        self.game = game
        self.warnings = warnings
        self.errors = errors
Run Code Online (Sandbox Code Playgroud)

并在使用中:

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
Run Code Online (Sandbox Code Playgroud)

这类似于subprocess模块处理错误的方式.

为了能够在游戏致命错误后检索和解析后续游戏,我建议您更改API:

  • 有一个游戏迭代器,只返回每个游戏的原始数据(它只需要知道如何判断一个游戏何时结束,下一个游戏开始)
  • 让解析器获取原始游戏数据并解析它(因此它不再负责你碰巧在文件中的位置)

这样,如果你有一个五个游戏的文件和两个游戏死亡,你仍然可以尝试解析游戏3,4和5.