python中关于错误与成功的返回值的最佳实践

Best practice in python for return value on error vs. success

一般来说,假设您有如下方法。

1
2
3
4
5
6
7
8
9
def intersect_two_lists(self, list1, list2):
    if not list1:
        self.trap_error("union_two_lists: list1 must not be empty.")
        return False
    if not list2:
        self.trap_error("union_two_lists: list2 must not be empty.")
        return False
    #http://bytes.com/topic/python/answers/19083-standard
    return filter(lambda x:x in list1,list2)

在这个特定的方法中,当发现错误时,我不想返回空列表,在这种情况下,因为这可能是这个特定方法调用的真正答案,我想返回一些东西来指示参数不正确。所以在本例中,我返回了false-on-error,另外还返回了一个列表(空的或不空的)。

我的问题是,在这样的领域,什么是最佳实践,而不仅仅是列表?返回我想要的任何东西,并确保我记录下来供用户阅读?:-)你们大多数人都做什么:

  • 如果成功的话,你应该返回"真"或"假",然后你会发现一个错误?
  • 如果成功的话,你应该返回一个列表,然后发现一个错误?
  • 如果成功的话,您应该返回一个文件句柄,然后捕获一个错误?
  • 等等

  • 首先,无论您做什么,都不会返回结果和错误消息。这是一种处理错误的非常糟糕的方法,会让你头痛不已。如果需要指示错误,请始终引发异常。

    除非必要,否则我通常会避免犯错误。在您的示例中,并不真正需要抛出错误。将空列表与非空列表相交不是错误。结果只是空列表,这是正确的。但假设你想处理其他案件。例如,如果方法具有非列表类型。在这种情况下,最好提出一个例外。例外没有什么可怕的。

    我的建议是查看Python库中类似的函数,并了解Python如何处理这些特殊情况。例如,看看集合中的交集方法,它往往是可以原谅的。这里,我试图将一个空集合与一个空列表相交:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> b = []
    >>> a = set()
    >>> a.intersection(b)
    set([])

    >>> b = [1, 2]
    >>> a = set([1, 3])
    >>> a.intersection(b)
    set([1])

    只有在需要时才会引发错误:

    1
    2
    3
    4
    5
    >>> b = 1
    >>> a.intersection(b)
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    TypeError: 'int' object is not iterable

    当然,有些情况下,成功或失败时返回"真"或"假"可能是好事。但保持一致是非常重要的。函数应始终返回相同的类型或结构。拥有一个可以返回列表或布尔值的函数是非常令人困惑的。或者返回相同的类型,但如果出现错误,该值的含义可能不同。

    编辑:

    OP说:

    I want to return something to indicate
    the parameters were incorrect.

    没有什么比异常更能说明错误。如果要指示参数不正确,请使用异常并显示一条有用的错误消息。在这种情况下返回结果只是令人困惑。在其他情况下,您可能希望指示什么都没有发生,但这不是一个错误。例如,如果您有一个从表中删除条目的方法,并且请求删除的条目不存在。在这种情况下,在成功或失败时返回"真"或"假"可能比较好。这取决于应用程序和预期行为


    引发异常比返回特殊值要好。这正是异常设计的目的,用更健壮和结构化的错误处理机制替换错误代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class IntersectException(Exception):
        def __init__(self, msg):
            self.msg = msg
        def __str__(self):
            return self.msg

    def intersect_two_lists(self, list1, list2):
        if not list1: raise IntersectException("list1 must not be empty.")
        if not list2: raise IntersectException("list2 must not be empty.")

        #http://bytes.com/topic/python/answers/19083-standard
        return filter(lambda x:x in list1,list2)

    在这种特定的情况下,我可能会放弃测试。实际上,交叉空列表没有什么问题。另外,如今,lambda有点不受欢迎,而倾向于列举理解。参见查找两个列表的交集?在不使用lambda的情况下编写此命令的几种方法。


    我喜欢返回一个元组:

    (True, some_result)

    (False, some_useful_response)

    一些有用的响应对象可以用来处理返回条件,也可以用来显示调试信息。

    注意:此技术适用于任何类型的返回值。它不应该被误认为是例外情况。

    在接收端,您只需打开:

    Code, Response = some_function(...)

    这种技术适用于"正常"控制流:当发生一些意外的输入/处理时,必须使用异常功能。

    同样值得注意的是:这种技术有助于规范化函数返回。程序员和函数的用户都知道要期望什么。

    免责声明:我来自二郎的背景:—)


    异常肯定比状态返回更好(而且更像是Python)。关于这方面的更多信息:异常与状态返回


    一般情况下,例外情况除外。我希望我能记住准确的引用(或谁说过),但您应该努力实现接受尽可能多的值和类型的函数,并且保持一个非常狭义的行为。这是纳迪亚所说的另一种说法。考虑您的函数的以下用法:

  • intersect_two_lists(None, None)
  • intersect_two_lists([], ())
  • intersect_two_lists('12', '23')
  • intersect_two_lists([1, 2], {1: 'one', 2: 'two'})
  • intersect_two_lists(False, [1])
  • intersect_two_lists(None, [1])
  • 我希望(5)抛出一个异常,因为传递False是一个类型错误。然而,其余的部分有某种意义,但它实际上取决于函数所表示的契约。如果intersect_two_lists被定义为返回两个iterables的交集,那么除(5)以外的所有东西都应该工作,只要你使None成为空集合的有效表示。实现方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    def intersect_two_lists(seq1, seq2):
        if seq1 is None: seq1 = []
        if seq2 is None: seq2 = []
        if not isinstance(seq1, collections.Iterable):
            raise TypeError("seq1 is not Iterable")
        if not isinstance(seq2, collections.Iterable):
            raise TypeError("seq1 is not Iterable")
        return filter(...)

    我通常编写助手函数来强制执行契约,然后调用它们来检查所有的前提条件。类似:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def require_iterable(name, arg):
       """Returns an iterable representation of arg or raises an exception."""
        if arg is not None:
            if not isinstance(arg, collections.Iterable):
                raise TypeError(name +" is not Iterable")
            return arg
        return []

    def intersect_two_lists(seq1, seq2):
        list1 = require_iterable("seq1", seq1)
        list2 = require_iterable("seq2", seq2)
        return filter(...)

    您还可以扩展这个概念,并将"policy"作为可选参数传入。我不建议这样做,除非你愿意接受基于策略的设计。我确实想提一下,以防你以前没有研究过这个选项。

    如果intersect_two_lists的合同只接受两个非空的list参数,那么,如果违反合同,应明确并提出例外:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def require_non_empty_list(name, var):
        if not isinstance(var, list):
            raise TypeError(name +" is not a list")
        if var == []:
            raise ValueError(name +" is empty")

    def intersect_two_lists(list1, list2):
        require_non_empty_list('list1', list1)
        require_non_empty_list('list2', list2)
        return filter(...)

    我认为这个故事的寓意是无论你做什么,都要始终如一地做,并且要明确。就我个人而言,我通常倾向于在合同被违反或我被赋予了一个我真正无法使用的价值时提出例外。如果我得到的价值是合理的,那么我会尝试做一些合理的回报。您可能还想在异常上读取C++ FAQLite条目。这个特别的条目为您提供了更多的食物来考虑例外情况。


    对于集合(列表、集合、dict等),返回空集合是一个明显的选择,因为它允许调用站点逻辑远离防御逻辑。更明确地说,空集合仍然是期望集合来自的函数的完美答案,您不必检查结果是否属于任何其他类型,并且可以以干净的方式继续您的业务逻辑。

    对于非收集结果,有几种方法可以处理条件返回:

  • 正如许多答案已经解释过的那样,使用异常是解决这一问题的一种方法,而且是惯用的Python。但是,我倾向于不要对控制流使用异常,因为我发现它会对异常的意图造成歧义。相反,在实际的异常情况下提出异常。
  • 另一种解决方案是返回None,而不是您预期的结果,但这迫使用户在其调用站点的任何地方添加防御检查,从而使他们试图实际执行的实际业务逻辑变得模糊。
  • 第三种方法是使用只能保存单个元素(或显式为空)的集合类型。这被称为可选方法,是我的首选方法,因为它允许您保持干净的调用站点逻辑。但是,python没有内置的可选类型,所以我使用自己的类型。如果有人想试一试的话,我把它发布在一个叫做optional.py的小图书馆里。您可以使用pip install optional.py安装它。我欢迎评论、功能请求和贡献。