【问题标题】:Why should functions always return the same type?为什么函数总是返回相同的类型?
【发布时间】:2010-12-22 19:06:22
【问题描述】:

我在某处读到,函数应该总是只返回一种类型 所以下面的代码被认为是坏代码:

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return None

我想更好的解决方案是

def x(foo):
 if 'bar' in foo:
  return (foo, 'bar')
 return ()

返回 None 然后创建一个新的空元组会不会更节省内存,或者这个时间差是否太小以至于即使在较大的项目中也无法注意到?

【问题讨论】:

    标签: python function return-type


    【解决方案1】:

    为什么函数应该返回一致类型的值?满足以下两条规则。

    规则 1——一个函数有一个“类型”——输入映射到输出。它必须返回一致类型的结果,否则它不是函数。真是一团糟。

    在数学上,我们说某个函数 F 是从域 D 到范围 R 的映射。F: D -> R。域和范围构成函数的“类型”。输入类型和结果类型对于函数的定义与名称或主体一样重要。

    规则 2 - 当您遇到“问题”或无法返回正确结果时,引发异常。

    def x(foo):
        if 'bar' in foo:
            return (foo, 'bar')
         raise Exception( "oh, dear me." )
    

    您可以打破上述规则,但长期可维护性和可理解性的成本是天文数字。

    “返回 None 不是更节省内存吗?”错误的问题。

    重点是不是以清晰、可读、明显的代码为代价来优化内存。

    【讨论】:

    • +1 表示最后一句话。可维护性是 99% 的代码应该考虑的真正成本。
    • 你还没有真正给出这个结论的任何理由。您为什么一个函数应该返回相同类型的原因包括 1)“否则任何函数都是一团糟”,以及 2)“它必须返回相同的类型以适应函数的数学定义。”第一个是纯粹的修辞,第二个引出了问题。并不是我不同意。我很乐意听从你的蟒蛇实力。但是你能给出一些实际的理由来解释为什么人们应该遵守这种做法吗?例如,打破这种做法肯定不会总是使代码不可读。
    • -1:当在这个问题的上下文中使用“函数”这个词时,可以合理地假设它表示 Python 函数,而不是数学函数。 Python 函数并不总是返回相同的类型是很正常的(例如copy.copy())。你的规则 1 说一些 Python 函数不是真正的函数,这是一个语义混乱。
    • copy.copy() 仍然是一个函数。签名(域和范围)是固定的。对于给定的域值,范围是可预测的。是的,有很多组合,但这些组合很容易预测。它是一个在数学意义上具有简单、可预测签名的函数。它不会自发返回不可预测的类型。
    • 标准库中也有很多例子要么返回一种类型,要么返回None。与 OP 的功能相比,它们不再“自发返回不可预测的类型”。例如os.getenv', re.search, sys.callstats. Raising an exception instead is a fine thing to do, but it's a design choice, and returning None` 通常是相当合理的。
    【解决方案2】:

    函数必须总是返回有限类型的对象,或者返回 None 是错误的,这不是很清楚。例如,re.search 可以返回 _sre.SRE_Match 对象或 NoneType 对象:

    import re
    match=re.search('a','a')
    
    type(match)
    # <type '_sre.SRE_Match'>
    
    match=re.search('a','b')
    
    type(match)
    # <type 'NoneType'>
    

    这样设计,你可以用成语来测试匹配

    if match:
        # do xyz
    

    如果开发人员要求 re.search 返回一个 _sre.SRE_Match 对象,那么 成语必须改为

    if match.group(1) is None:
        # do xyz
    

    要求 re.search 始终返回 _sre.SRE_Match 对象不会有任何重大收获。

    所以我认为你如何设计函数必须取决于情况,特别是你打算如何使用函数。

    还要注意_sre.SRE_Match 和NoneType 都是对象的实例,所以在广义上它们属于同一类型。所以“函数应该总是只返回一种类型”的规则是毫无意义的。

    话虽如此,返回所有共享相同属性的对象的函数非常简单。 (鸭子类型,而不是静态类型,是 python 的方式!)它可以让你将函数链接在一起:foo(bar(baz))) 并确定你将在另一端收到的对象的类型。

    这可以帮助您检查代码的正确性。通过要求函数只返回某种有限类型的对象,需要检查的情况更少。 “foo 总是返回一个整数,所以只要在我使用 foo 的任何地方都需要一个整数,我就是金……”

    【讨论】:

    • 我认为这是最好的答案,因为它解决了 Python 范式。返回 None 通常很好,无论函数返回什么,因为测试返回值是常见的做法。例如x = foo() if x: # do something 除了 0、()、"" 或 None 之外的任何值都等于 True。
    • 即使re.search 总是返回一个匹配对象,它仍然可以用if match 检查——如果__nonzero__ 函数被相应地实现。
    【解决方案3】:

    函数应返回什么的最佳实践因语言而异,甚至在不同的 Python 项目之间也存在很大差异。

    对于一般的 Python,我同意这样一个前提,即如果您的函数通常返回可迭代对象,则返回 None 是不好的,因为没有测试的迭代变得不可能。在这种情况下只返回一个空的迭代,如果你使用 Python 的标准真值测试,它仍然会测试 False:

    ret_val = x()
    if ret_val:
         do_stuff(ret_val)
    

    并且仍然允许您在不测试的情况下对其进行迭代:

    for child in x():
        do_other_stuff(child)
    

    对于可能返回单个值的函数,我认为返回 None 是完全可以接受的,只需在您的文档字符串中记录这可能发生的情况。

    【讨论】:

    • 我真的很喜欢你提出的可迭代与单值参数,但我认为它需要稍微调整一下——如果可迭代是一个元组怎么办?用于返回元组的函数的常见 python 主义是 a, b = x(),如果返回 None,它将引发 TypeError 异常。此外,字符串是可迭代的和单个值。也许参数应该是动态长度可迭代与单个值?或者,更一般地说,可变与不可变?
    • 如果你返回一个固定大小的元组,那么是的,我同意,返回 None 是个坏主意,但这只是“如果调用者想在不测试的情况下破坏你的响应”的特例,这已经被“如果它是可迭代的,则不要返回 None”所涵盖。关于字符串,我认为返回空字符串通常比返回 None 更好,但是 very 偶尔空字符串和 None 有不同的含义,在这种情况下,您应该再次记录这一点。
    • ...'which is already covered by "don't return None if it's iterable".' 哦,对了。我的脑子一定漏了一拍。好吧,我猜字符串点仍然存在,谢谢你的介绍。
    【解决方案4】:

    这是我对这一切的看法,我还将尝试解释为什么我认为接受的答案大多是不正确的。

    首先programming functions != mathematical functions。最接近数学函数的方法是进行函数式编程,但即便如此,也有很多例子表明并非如此。

    • 函数不必必须有输入
    • 函数不必必须有输出
    • 函数不必将输入映射到输出(因为前面两个要点)

    就编程而言,函数可以简单地视为一块内存,具有开始(函数的入口点)、主体(空或其他)和出口点(一个或多个,具体取决于实现)这些是为了重用您编写的代码。即使你没有看到它,一个函数也总是“返回”一些东西。这实际上是函数调用之后下一条语句的地址。如果您使用汇编语言进行一些非常低级的编程,您将看到它的全部荣耀(我敢于让您加倍努力,像 Linus Torvalds 那样手动编写一些机器代码,他在他的研讨会和采访:D)。此外,您还可以获取一些输入并吐出一些输出。这就是为什么

    def foo():
      pass
    

    是一段完全正确的代码。

    那么为什么返回多种类型是不好的呢?嗯......除非你滥用它,否则它根本不是。这当然是编程技能差和/或不知道您使用的语言可以做什么的问题。

    返回 None 然后创建一个新的空元组会不会更节省内存,或者这个时间差是否太小以至于即使在较大的项目中也无法注意到?

    据我所知 - 是的,返回 NoneType 对象在内存方面会便宜得多。这是一个小实验(返回值是字节):

    >> sys.getsizeof(None)
    16
    >> sys.getsizeof(())
    48
    

    根据您用作返回值的对象类型(数字类型、列表、字典、元组等),Python 以不同的方式管理内存,包括最初保留的存储。

    但是,您还必须考虑函数调用周围的代码以及它如何处理函数返回的任何内容。你检查NoneType吗?或者您只是检查返回的元组的长度是否为 0?返回值及其类型的这种传播(NoneType 与您的情况下的空元组)实际上可能更繁琐处理和炸毁在您的脸上。不要忘记 - 代码本身已加载到内存中,因此如果处理 NoneType 需要太多代码(即使是少量代码,但数量很大)最好保留空元组,这也可以避免混淆人们使用你的函数却忘记了它实际上返回了两种类型的值。

    说到返回多种类型的值,这是我同意接受的答案的部分(但只是部分) - 返回单一类型无疑会使代码更易于维护。只检查类型 A 然后检查 A、B、C 等要容易得多。

    然而,Python 是一种面向对象的语言,因此继承、抽象类等以及作为整个 OOP 恶作剧一部分的所有内容都会发挥作用。它甚至可以即时生成类,这是我几个月前发现的,当时我惊呆了(在 C/C++ 中从未见过这种东西)。

    旁注:您可以在this nice overview article 中阅读一些关于元类和动态类的信息,其中包含大量示例。

    事实上,如果没有所谓的多态函数,甚至不存在多种设计模式和技术。下面我给你两个非常热门的话题(找不到更好的方法来用一个词来概括两者):

    • Duck typing - 通常是 Python 代表的动态类型语言的一部分
    • Factory 方法设计模式 - 基本上它是一个根据接收到的输入返回各种对象的函数。

    最后,你的函数是返回一种还是多种类型完全取决于你要解决的问题。这种多态行为可以被滥用吗?当然,就像其他一切一样。

    【讨论】:

    • “返回 NoneType 对象在内存方面会便宜得多。” - 不正确,两者都花费 8 个字节(PyObject * 指针),并且两者都花费 ()和None 是在启动时分配的全局常量。
    【解决方案5】:

    我个人认为函数返回元组或无是完全可以的。但是,一个函数最多应该返回 2 种不同的类型,而第二种应该是 None。例如,函数不应返回字符串和列表。

    【讨论】:

    • 另外,一个返回 3-uple 或 0-uple 的函数,如示例中所示,对我来说看起来很糟糕。在这个例子中,“return None”也是我的首选。
    【解决方案6】:

    如果x 是这样调用的

    foo, bar = x(foo)
    

    返回None 将导致

    TypeError: 'NoneType' object is not iterable
    

    如果'bar' 不在foo 中。

    示例

    def x(foo):
        if 'bar' in foo:
            return (foo, 'bar')
        return None
    
    foo, bar = x(["foo", "bar", "baz"])
    print foo, bar
    
    foo, bar = x(["foo", "NOT THERE", "baz"])
    print foo, bar
    

    这会导致:

    ['foo', 'bar', 'baz'] bar
    Traceback (most recent call last):
      File "f.py", line 9, in <module>
        foo, bar = x(["foo", "NOT THERE", "baz"])
    TypeError: 'NoneType' object is not iterable
    

    【讨论】:

      【解决方案7】:

      过早的优化是万恶之源。微小的效率提升可能很重要,但直到你证明你需要它们。

      无论您使用哪种语言:函数只定义一次,但往往会在任意数量的地方使用。拥有一致的返回类型(更不用说记录的前置条件和后置条件)意味着您必须花费更多的精力定义函数,但大大简化了函数的用法。猜一猜一次性成本是否会超过重复节省的成本...?

      【讨论】:

        猜你喜欢
        • 2020-04-06
        • 1970-01-01
        • 2014-11-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-26
        • 2013-09-20
        相关资源
        最近更新 更多