【问题标题】:What's the Pythonic way to report nonfatal errors in a parser?在解析器中报告非致命错误的 Pythonic 方法是什么?
【发布时间】:2014-12-09 01:03:00
【问题描述】:

我创建的解析器从文件中读取记录的国际象棋游戏。 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)
# ...

有时会遇到非法移动(或其他问题)。有什么好的 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.
       # ...
    

这些或其他方法中的一些是否已在野外使用?

【问题讨论】:

  • 有许多不同的选项 - 使用logging(例如,只是警告无法解析游戏某某),还有一个额外的suppress_errors 参数,...您想要返回部分阅读的游戏吗?刚刚跳过?这应该是一个选择吗?这太宽泛了,无法真正正确回答——你必须决定你想让你的 API 做什么(然后记录下来!)
  • 第三个选项不是必须的;您可以定义一个自定义异常,其中包含parser_info 将包含的任何信息。
  • 一篇不错的博文在这里。 jeffknupp.com/blog/2013/02/06/… 这个问题实际上是 LBYL 与 EAFP 的风格问题。 Python 更喜欢 EAFP,并且更简洁地引发异常或通过。我认为在实例化游戏之前将异常列表编译回用户是一个更好的选择。也许使用 while info.error: 继续将输入发送回用户。这不是致命的,只是验证。

标签: python exception-handling error-handling warnings


【解决方案1】:

我不确定该解决方案是否是 Pythonic,但我经常使用它并稍作修改:解析器在生成器中完成其工作并产生结果和状态代码。接收代码决定如何处理失败的项目:

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

更通用的方法是练习使用设计模式。 Observer 的简化版本(当您为特定错误注册回调时)或一种 Visitor(访问者有处理特定错误的方法,请参阅 SAX 解析器以获取见解)可能是一个清晰且易于理解的解决方案.

【讨论】:

  • @cat 我更新了我的答案并添加了一些想法。
【解决方案2】:

没有库,很难干净地做到这一点,但仍有可能。

根据情况有不同的处理方法。

方法一:

将while循环的所有内容放入以下内容:

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

方法二:

与方法 1 相同,但有多个 try/except 事物,因此它不会跳过太多代码并且您知道错误的确切位置。

抱歉,赶时间,希望对您有所帮助!

【讨论】:

  • -1 如果可以的话:我认为不够干净。此外,我们不想要 exceptions,我们想要 warnings,但即使我们将 warnings 提升为 exceptionswarningslogging 比仅处理异常要好得多。
  • @cat:OP 没有说想要 warnings 和不想要例外,赏金文本也没有。
【解决方案3】:

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

考虑到致命错误(文件已损坏)和警告(进行了非法移动,但后续移动显示与该移动一致(换句话说,用户错误并且当时没有人发现它))的可能性)我建议第一个和第二个选项的组合:

  • 在无法继续解析时引发异常
  • 收集任何不妨碍进一步解析的错误/警告,直到结束

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

return game, warnings, errors

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

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

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

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

自定义错误可能是:

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

并在使用中:

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

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

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

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

这样,如果您有一个包含 5 个游戏的文件并且游戏 2 死了,您仍然可以尝试解析游戏 3、4 和 5。

【讨论】:

  • 我同意这个答案:您的解析器应始终报告其执行情况,但如果无法完成工作,则应引发异常。要求对文件进行迭代的上层代码应了解可能引发的异常并做出相应的反应。
  • 现在接受这个。最佳答案可能因用例而异,这表明我的原始问题相当模棱两可或开放式。日志记录、警告和生成器答案也都很有用。
【解决方案4】:

最 Pythonic 的方式是 logging 模块。在 cmets 中已经提到过,但不幸的是没有足够强调这一点。比warnings 更可取的原因有很多:

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

这将打印如下消息:

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

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

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

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

突然之间,无需对模块代码进行任何更改,您的警告消息就会以不同格式保存到文件中,而不是打印到屏幕上:

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

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

logging.basicConfig(level = logging.ERROR)

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

【讨论】:

  • 另外,我现在正在调查它,但我认为我宁愿警告看起来像异常,而不是看起来像日志消息。 warnings 默认提供 traceback-y 样式,而 logging 用于单行日志消息。
  • @cat 基本使用场景的参与几乎是一样的。警告模块通常用于生成像you're using a deprecated HTTPS library which may be a security issue, please upgrade 这样的消息,这与这种情况完全不同。格式可通过logging 完全自定义。但是当然有很多方法可以给猫剥皮……没有双关语的意思。 :-)
  • @cat 没有什么能阻止您将回溯放入您的日志消息中——这是格式化的问题(并且可能在您的 logging.warning() 调用中添加一个额外的参数)。与日志记录相比,Warnings 模块默认会做更多的事情,但日志记录可以做所有这些事情,甚至更多,甚至不是一场比赛。我真的建议尝试日志记录 - 乍一看它看起来很不起眼,但它非常强大。
  • Welp,我要切换到logging :)
  • @Lav:虽然日志记录非常有用(添加它不会有什么坏处),但记录问题并不会告诉调用者发生了问题——这意味着调用者会尝试使用它返回的错误游戏,并且可能在其他地方出错。
【解决方案5】:

我提供赏金是因为我想知道这是否真的是最好的方法。然而,我也在写一个解析器,所以我需要这个功能,这就是我想出的。


warnings module 正是您想要的。

起初让我远离它的是文档中使用的每个示例警告看起来都像 these:

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

...这是不可取的,因为我不希望它是 UserWarning,我想要我自己的自定义警告名称。

解决方法如下:

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

给出:

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

【讨论】:

  • 谢谢,我已经忘记了这个问题。在我接受之前,让我们看看赏金是否吸引了其他答案。
  • @АндрейБеньковский Niklas 可能会至少等到我的赏金结束,不用担心
  • warnings 更多的是通知用户,而不是调用代码。如果您的模块依赖于x_caller() 的正确执行,则它不会知道发生了问题。
  • @EthanFurman 谢天谢地,就我的目的而言,几乎我的整个模块都是 NIH,并且任务关键的后端代码填充有 try: ... raise BadInternalCallException; except BadInternalCallException as e:print("fatal backend bug:", e)
  • 但是,如果后端出现问题,(希望)会被在程序每次运行之前运行的测试套件捕获,如果出现问题,则会以 AssertionError 退出。
猜你喜欢
  • 2018-12-26
  • 2012-07-20
  • 2021-09-28
  • 2014-07-10
  • 2019-01-28
  • 1970-01-01
  • 2014-09-28
  • 1970-01-01
  • 2014-04-29
相关资源
最近更新 更多