什么时候以及如何使用例外?


20

那个设定

我经常很难确定何时以及如何使用异常。让我们考虑一个简单的示例:假设我正在抓取一个网页,说“ http://www.abevigoda.com/ ”,以确定Abe Vigoda是否还活着。为此,我们要做的就是下载页面并寻找出现“ Abe Vigoda”短语的时间。我们返回首次亮相,因为其中包括安倍晋三的身份。从概念上讲,它将如下所示:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

where parse_abe_status(s)以“ Abe Vigoda is something ” 形式的字符串返回“ something ”部分。

在您争辩说有更多更好,更可靠的方法来抓取该页面以了解安倍晋三的身份之前,请记住,这只是一个简单而人为的示例,用于强调我所处的常见情况。

现在,此代码在哪里遇到问题?除其他错误外,一些“预期”错误是:

  • download_page可能无法下载该页面,并引发IOError
  • 该URL可能未指向正确的页面,或者该页面下载不正确,因此没有任何匹配。hits然后是空列表。
  • 该网页已被更改,可能使我们对该页面的假设错误。也许我们希望提到4次安倍维哥达,但现在我们发现5个。
  • 由于某些原因,它hits[0]可能不是“ Abe Vigoda is something ” 格式的字符串,因此无法正确解析。

第一种情况对我而言并不是真正的问题:IOError抛出该异常并可以由我的函数的调用者处理。因此,让我们考虑其他情况以及如何处理它们。但是首先,让我们假设我们parse_abe_status以最愚蠢的方式实现:

def parse_abe_status(s):
    return s[13:]

即,它不执行任何错误检查。现在,进入选项:

选项1:退货 None

我可以通过返回来告诉呼叫者出了点问题None

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

如果主叫方收到None来自我的职责,他应该假设没有提到阿伯·维哥达的,所以什么地方出了错。但这很模糊,对不对?这hits[0]对我们认为不是的情况没有帮助。

另一方面,我们可以添加一些例外:

选项2:使用例外

如果hits为空,IndexError则在尝试时将抛出hits[0]。但是不应期望调用者处理IndexError我的函数引发的异常,因为他不知道该异常的IndexError来源。就find_all_mentions他所知,它可能是被抛出的。因此,我们将创建一个自定义异常类来处理此问题:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

现在,如果页面已更改且命中次数意外,该怎么办?这并不是灾难性的,因为代码仍然可以工作,但是调用者可能想要格外小心,或者他可能希望记录警告。所以我会警告:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

最后,我们可能会发现它status不是活着的还是死亡的。也许出于某种奇怪的原因,今天竟然是comatose。然后我不想返回False,因为这意味着安倍已经死了。我该怎么办?可能引发异常。那是什么 我应该创建一个自定义的异常类吗?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

选项3:介于两者之间

我认为第二种方法(带异常)是可取的,但是我不确定是否在其中正确使用了异常。我很好奇看到更有经验的程序员将如何处理这个问题。

Answers:


17

Python中的建议是使用异常来指示失败。即使您期望定期失败,也是如此。

从代码调用者的角度来看它:

my_status = get_abe_status(my_url)

如果我们返回None怎么办?如果调用者没有专门处理get_abe_status失败的情况,它将简单地尝试将my_stats设置为None继续。以后可能会产生难以诊断的错误。即使您检查无,此代码也不知道为什么get_abe_status()失败。

但是,如果我们提出例外情况怎么办?如果调用者没有专门处理此情况,则异常将向上传播,最终到达默认的异常处理程序。那可能不是您想要的,但是比在程序的其他地方引入一个细微的错误要好。此外,该异常还提供有关在第一个版本中丢失的错误信息。

从调用者的角度来看,获取异常比返回值更方便。这就是python样式,用于使用异常指示失败条件而不返回值。

有些人会采取不同的观点,并认为您只应在从未真正希望发生的情况下使用例外。他们认为,正常的跑步不会引起任何异常。给出这个原因的一个原因是异常的效率非常低,但是对于Python而言实际上并非如此。

您的代码有几点:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

这是一种检查空白列表的真正令人困惑的方法。不要仅仅为了检查某些内容而导致异常。使用if。

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

您确实意识到logger.warning行永远不会正确运行吗?


1
感谢(迟迟没有)您的回复。与查看已发布的代码一起,它改善了我何时以及如何引发异常的感觉。
jme 2014年

4

接受的答案应该被接受并回答问题,我写这个只是为了提供一些额外的背景。

Python的信条之一是:宽恕要比许可容易。这意味着通常您只做某事,并且如果您期望例外,则可以处理它们。而不是事先进行if检查以确保您不会遇到异常。

我想提供一个示例,向您展示与C ++ / Java在心态上的巨大差异。C ++中的for循环通常看起来像:

for(int i = 0; i != myvector.size(); ++i) ...

一种思考的方式:访问myvector[k]k> = myvector.size()会导致异常。因此,原则上您可以(很尴尬)将其编写为try-catch。

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

或类似的东西。现在,考虑一下python for循环中发生的事情:

for i in range(1):
    ...

怎么运作的?for循环获取range(1)的结果并对其调用iter(),以获取其迭代器。

b = range(1).__iter__()

然后,它在每次循环迭代时都对其进行调用,直到...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

换句话说,Python中的for循环实际上是一种伪装的try-except。

就具体问题而言,请记住,异常会停止正常的函数执行,必须将其单独处理。在Python中,只要没有点执行函数中的其余代码,和/或没有任何返回值可正确反映函数中发生的情况,则应随意抛出它们。请注意,从函数中提早返回是不同的:提早返回意味着您已经找到了答案,不需要其余代码来找出答案。我说的是,当答案未知时,应该抛出异常,并且确定答案的其余代码无法合理运行。现在,“正确反映”自身,就像您选择抛出的异常一样,都由文档决定。

对于您的特定代码,我想说任何导致匹配为空列表的情况都应该抛出。为什么?好的,您的功能设置方式是在没有解析匹配的情况下无法确定答案的。因此,如果由于URL错误或由于hits为空而无法对hits进行解析,则该函数将无法回答问题,实际上甚至无法进行尝试。

在这种特殊情况下,我会辩称,即使您设法解析并且没有得到合理的答案(有效还是无效),那么您仍然应该抛出异常。为什么?因为,该函数返回一个布尔值。不退货对您的客户非常危险。如果他们对None进行if检查,则不会失败,它将被默默地视为False。因此,您的客户基本上将始终必须执行if is None检查是否不希望出现静默失败...因此,您可能应该抛出。


2

当你应该使用异常出色的发生。也就是说,在正确使用应用程序的情况下不应该发生的事情。如果您的方法的使用者允许并期望搜索不会被发现的内容,则“未找到”不是例外情况。在这种情况下,您应该返回null或“ None”或{},或表示空返回集的内容。

另一方面,如果您确实希望您的方法的使用者总是(除非他们以某种方式搞砸了)找到要搜索的内容,那么没有发现它是一个例外,您应该这样做。

关键是异常处理可能会很昂贵-异常应该在发生时收集有关应用程序状态的信息(例如堆栈跟踪),以帮助人们了解发生异常的原因。我认为这不是您要尝试的。


1
如果您决定不找值是允许的,那么请注意使用什么来表明发生了什么。如果您的方法应该返回a String且您选择“ None”作为指标,则意味着您必须注意“ None”永远不会是有效值。另请注意,在查看数据与未找到值之间以及在无法检索数据之间存在差异,因此我们无法找到数据。在这两种情况下具有相同的结果意味着一旦期望值一无所获,您将无处可见。
unholysampler

内联代码块标记有反引号(`),也许这就是您要对“无”进行的处理?
2013年

3
恐怕在Python中这绝对是错误的。您正在将C ++ / Java样式推理应用于另一种语言。Python使用异常来指示for循环的结束;那真是无与伦比。
尼尔·弗里德曼2014年

2

如果我在写一个函数

 def abe_is_alive():

我将它写入return TrueFalse在我绝对肯定一方或另一方的情况下,和raise在其他情况下(例如错误raise ValueError("Status neither 'dead' nor 'alive'"))。这是因为调用我的函数期望一个布尔值,并且如果我不能确定地提供该布尔值,则常规程序流程不应继续。

就像您的示例所得到的“点击”数量与预期的不同,我可能会忽略;只要其中一个热门歌曲仍符合我的模式“ Abe Vigoda is {dead | alive}”,就可以了。这样可以重新排列页面,但仍可以获取适当的信息。

而不是

try:
    hits[0] 
except IndexError:
    raise NotFoundError

我会明确检查:

if not hits:
    raise NotFoundError

因为这倾向于“便宜”,然后设置try

我同意你的看法IOError; 我也不会尝试处理连接到该网站的错误-如果由于某种原因我们不能处理该网站(因为它不能帮助我们回答问题),那么它就不合适了,它应该可以通过转到调用函数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.