35

我在某处读到函数应始终只返回一种类型,因此以下代码被视为错误代码:

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

我想更好的解决方案是

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

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

4

7 回答 7

53

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

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

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

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

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

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

“返回 None 不是更便宜的记忆吗?” 错误的问题。

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

于 2009-12-03T11:29:08.363 回答
23

一个函数必须总是返回有限类型的对象,或者返回 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

_sre.SRE_Match要求 re.search 总是返回一个对象不会有任何重大收获。

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

另请注意,两者_sre.SRE_MatchNoneType都是对象的实例,因此在广义上它们属于同一类型。所以“函数应该总是只返回一种类型”的规则是相当没有意义的。

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

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

于 2009-12-03T14:42:41.733 回答
13

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

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

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

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

for child in x():
    do_other_stuff(child)

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

于 2009-12-03T15:35:46.667 回答
9

这是我对所有这些的想法,我还将尝试解释为什么我认为接受的答案大多是不正确的。

首先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++ 中从未见过这种东西)。

旁注:您可以在这篇不错的概述文章中阅读一些有关元类和动态类的信息,其中包含大量示例。

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

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

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

于 2015-09-19T23:01:24.550 回答
6

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

于 2009-12-03T15:39:35.223 回答
4

如果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
于 2009-12-03T11:20:12.323 回答
2

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

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

于 2010-07-06T14:32:32.887 回答