从Python函数返回多个值的替代方案

1217

在支持多返回值的编程语言中,通常使用元组来实现。

选项:使用元组

考虑以下简单示例:

def f(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return (y0, y1, y2)

然而,当返回的值的数量增加时,这很快会变得棘手。如果您想返回四个或五个值怎么办?当然,您可以继续使用元组,但很容易忘记哪个值在哪里。在任何想要接收它们的地方解包它们也很丑陋。

选项:使用字典

下一个合乎逻辑的步骤似乎是引入某种“记录符号”。在Python中,显而易见的方法是通过 dict 实现。

考虑以下:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0': y0, 'y1': y1 ,'y2': y2}

仅为明确起见,y0、y1和y2只是抽象标识符。正如指出的那样,在实践中,您将使用有意义的标识符。

现在,我们有一种机制,可以投影出返回对象的特定成员。例如,

result['y0']

选项:使用类

然而,还有另一种选择。我们可以返回一个专门的数据结构。我已经在Python的语境中描述了这个方法,但我相信它同样适用于其他编程语言。事实上,如果你在C语言中工作,这可能是你唯一的选择。下面是具体实现:

class ReturnValue:
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return ReturnValue(y0, y1, y2)

在Python中,前两者在管道方面可能非常相似 - 毕竟{ y0,y1,y2 }最终只是成为ReturnValue的内部__dict__中的条目。
但是,Python为微小对象提供了一个额外的功能,即__slots__属性。该类可以表示为:
class ReturnValue(object):
  __slots__ = ["y0", "y1", "y2"]
  def __init__(self, y0, y1, y2):
     self.y0 = y0
     self.y1 = y1
     self.y2 = y2

来自Python参考手册

__slots__声明接受一系列实例变量,并为每个变量保留足够的空间以容纳一个值。由于不会为每个实例创建__dict__,因此可以节省空间。

选项:使用dataclass(Python 3.7+)

使用Python 3.7的新数据类,返回一个具有自动添加的特殊方法、类型和其他有用工具的类:

@dataclass
class Returnvalue:
    y0: int
    y1: float
    y3: int

def total_cost(x):
    y0 = x + 1
    y1 = x * 3
    y2 = y0 ** y3
    return ReturnValue(y0, y1, y2)

选项:使用列表

另一个我忽略的建议来自于蜥蜴比尔:

def h(x):
  result = [x + 1]
  result.append(x * 3)
  result.append(y0 ** y3)
  return result

这虽然是一种方法,但它是我最不喜欢的。我想我被接触 Haskell 污染了,但混合类型列表的想法始终让我感到不舒服。在这个特定的例子中,列表-不是-混合类型,但可以构想成是混合类型。

就我所知,在这种方式中使用的列表与元组相比并没有任何实质性的区别。Python 中列表和元组之间唯一的真正区别在于列表是 可变的,而元组则不是。

我个人倾向于沿用函数式编程的约定:对于同一类型的任意数量的元素,请使用列表,对于预定类型的固定数量的元素,请使用元组。

问题

在冗长的序言之后,必然会出现一个问题。你认为哪种方法最好?


10
在您出色的示例中,您使用了变量 y3,但是除非声明了全局变量 y3,否则会导致 NameError: global name 'y3' is not defined。也许只用 3 会更好? - hetepeperfan
@hetepeperfan 不需要改变3,也不需要在全局定义y3,你也可以使用本地名称y3,这样也能达到同样的效果。 - okie
14个回答

690

为此,2.6版本添加了命名元组。还可以参考os.stat作为类似的内置示例。

>>> import collections
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>> p = Point(1, y=2)
>>> p.x, p.y
1 2
>>> p[0], p[1]
1 2

在Python 3的最新版本(我认为是3.6+),新的typing库引入了NamedTuple类,使得创建命名元组更加容易且功能更强大。从typing.NamedTuple继承可以使用文档字符串、默认值和类型注解。
示例(来自文档):
class Employee(NamedTuple):  # inherit from typing.NamedTuple
    name: str
    id: int = 3  # default value

employee = Employee('Guido')
assert employee.id == 3

7
namedtuple的设计理念是为了在处理大量结果(如数据库查询结果的长列表)时拥有更小的内存占用。对于个别项(如果函数不经常调用),字典和类也可以很好地使用。但对于这种情况,命名元组也是一个好的/更好的解决方案。 - Lutz Prechelt
2
我认为这是最好的答案。有一件事我一开始没有意识到 - 你不需要在外层作用域中声明namedtuple; 你的函数本身可以定义容器并返回它。 - wom
14
@wom:不要这样做。Python不会努力使namedtuple定义唯一(每次调用都会创建一个新的),创建namedtuple类在CPU和内存方面相对昂贵,所有类定义本质上都涉及循环引用(因此,在CPython上,你需要等待循环GC运行以释放它们)。 它还使得无法pickle该类(因此,在大多数情况下,无法使用multiprocessing实例)。 在我的3.6.4 x64上每次创建该类耗时约0.337毫秒,并且占用将近1KB的内存,从而消耗任何实例节省。 - ShadowRanger
6
请注意,Python 3.7在改进了创建新的“namedtuple”类的速度CPU成本大约降低了4倍,但它们仍然比创建实例的成本高出大约1000倍,每个类的内存成本仍然很高(我上次评论中关于该类“小于1 KB”的说法是错误的,“_source”本身通常为1.5 KB;在3.7中删除了“_source”,因此每个类的创建成本可能更接近原来声称的稍低于1 KB)。 - ShadowRanger
2
@endolith 因为你可以在创建后添加值,这意味着你可以将结果添加到 retval 命名空间中,而无需等待一次性将它们放入命名元组中。有时候可以大大减少较大函数的混乱。 - jaaq
显示剩余3条评论

264

对于小型项目,我发现使用元组最容易处理。当元组变得难以管理时(而不是之前),我开始将事物分组到逻辑结构中,但我认为你建议使用字典和ReturnValue对象是错误的(或者过于简单化)。

返回具有键"y0""y1""y2"等的字典并不比元组具有任何优势。返回一个带有属性.y0.y1.y2等的ReturnValue实例也不比元组具有任何优势。如果您想要进一步发展,您需要开始命名事物,而您可以在元组中完成:

def get_image_data(filename):
    [snip]
    return size, (format, version, compression), (width,height)

size, type, dimensions = get_image_data(x)

在我看来,除了元组之外,唯一好的技术是返回具有适当方法和属性的真实对象,就像你从 re.match()open(file) 中得到的那样。


27
“返回一个带有y0、y1、y2等键的字典与使用元组相比没有任何优势”,这句话的意思是:使用字典作为返回结果,可以在不破坏现有代码的情况下向返回的字典中添加字段。 - ostrokach
6
“返回一个带有y0、y1、y2等键的字典与使用元组相比没有任何优势”,它也更易读且访问数据时不易出错,因为你是基于名称而不是位置来访问数据。 - Denis Dollfus

239

很多答案建议您返回一个集合,例如字典或列表。您可以省略额外的语法,只需写出逗号分隔的返回值即可。注意:这实际上返回一个元组。

def f():
    return True, False
x, y = f()
print(x)
print(y)

给出:

True
False

28
你仍然返回了一个集合,它是元组。我更喜欢用括号来使它更明确。尝试这样做:type(f()) 返回 <class 'tuple'> - Igor
29
@Igor:没有必要明确表明返回的是元组,重要的是你正在使用返回多个值的惯用语法。同样的原因也适用于交换 idiom,x, y = y, x,多个初始化 x, y = 0, 1 等。当然,它在幕后创建了元组,但没有必要明确地表明这一点,因为元组并不是关键点。Python 教程在讲解 tuple 之前很早就 介绍了多重赋值 - ShadowRanger
@ShadowRanger 在 Python 中,等号右侧由逗号分隔的任何值序列都是一个元组,无论是否在它们周围加上括号。因此,实际上没有明确或隐含的区别。a、b、c 和 (a、b、c) 一样是元组。当您返回这些值时,也没有“在幕后”制作元组,因为它只是一个简单的元组。OP 已经提到了元组,所以他提到的内容和这个答案展示的内容之间实际上没有区别。 - Ken4scholars
@Ken4scholars:是的,语言定义将它们视为等效的。话虽如此,实际的程序员倾向于将括号视为表示序列数据结构,而没有括号,则只是“多个东西”。我同意行为上没有区别,但额外的括号改变了我认识的大多数人对它的看法。同意它在行为上与OP的示例没有区别(具有OP不喜欢的所有相同限制)。 - ShadowRanger
2
这实际上是问题中建议的第一个选项。 - endolith
1
@endolith 这个答案回答了这个人两次提出的问题(“如何返回多个值?”和“你如何返回多个值?”)。问题的文本有时会发生变化。而且这是一个基于观点的问题。 - Joseph Hansen

83

我支持使用字典。

我发现,如果我编写的函数需要返回超过2-3个变量,我会将它们放在一个字典中。否则,我很容易忘记返回值的顺序和内容。

此外,引入“特殊”的数据结构会使代码更难理解。(其他人将不得不查找代码以了解它是什么)

如果你担心类型查找,请使用描述性的字典键,例如,“x-values list”。

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }

6
经过多年的编程,我倾向于按照数据和功能所需的结构进行编程。首先考虑功能,随时可以根据需要进行重构。 - monkut
如何在不多次调用函数的情况下获取字典中的值?例如,如果我想在另一个函数中使用y1和y3呢? - Matt
4
将结果分配给一个单独的变量。result = g(x); other_function(result) - monkut
1
@monkut 是的。这种方式还允许将结果传递给多个函数,这些函数从结果中获取不同的参数,而无需每次都特别引用特定的结果部分。 - Gnudiff

42

另外一个选择是使用生成器:

>>> def f(x):
        y0 = x + 1
        yield y0
        yield x * 3
        yield y0 ** 4


>>> a, b, c = f(5)
>>> a
6
>>> b
15
>>> c
1296
尽管在我看来元组通常是最好的选择,除非返回的值有可能被封装成类的实例。

27
这可能很“干净”,但看起来一点也不直观。从来没有遇到过这种模式的人怎么知道自动元组解包会触发每个 yield - coredumperror
1
@CoreDumpError,生成器就是生成器。在 def f(x): …; yield b; yield a; yield r(g for g in [b, a, r]) 之间没有外部区别,两者都可以轻松转换为列表或元组,并且支持元组解包。元组生成器形式遵循函数式方法,而函数形式是命令式的,将允许流程控制和变量赋值。 - sleblanc
非常有创意!除了需要打更多的字之外,跟返回一个元组一样,既不更好也不更差。 - Roland

39

我更喜欢:

def g(x):
  y0 = x + 1
  y1 = x * 3
  y2 = y0 ** y3
  return {'y0':y0, 'y1':y1 ,'y2':y2 }

看起来其他所有的代码都是为了做同样的事情而存在的。


28
元组更易于解包: y0,y1,y2 = g() 使用字典则需要执行以下操作: result = g() y0,y1,y2 = result.get('y0'),result.get('y1'),result.get('y2') 这有点丑陋。每个解决方案都有它的优缺点。 - Oli

32

每当元组感觉“自然”时,我更喜欢使用元组;坐标是典型的例子,其中单独的对象可以自立其身,例如在单轴缩放计算中,且顺序很重要。注意:如果我可以对项目进行排序或洗牌而不会对组的含义产生负面影响,那么我可能不应该使用元组。

仅在分组对象不总是相同的情况下,我才将字典用作返回值。考虑可选电子邮件标头。

对于其余情况,在组内具有固有含义或需要具有自己方法的完整对象的情况下,我使用类。


29
>>> def func():
...    return [1,2,3]
...
>>> a,b,c = func()
>>> a
1
>>> b
2
>>> c
3

24

通常情况下,“专业结构”实际上是一个对象的合理当前状态,具有自己的方法。

class Some3SpaceThing(object):
  def __init__(self,x):
    self.g(x)
  def g(self,x):
    self.y0 = x + 1
    self.y1 = x * 3
    self.y2 = y0 ** y3

r = Some3SpaceThing( x )
r.y0
r.y1
r.y2

我喜欢为匿名结构体找到名称,如果可能的话。有意义的名称可以使事情更加清晰。


21

Python的元组、字典和对象为程序员提供了一种在小型数据结构("things")中在正式性和方便性之间平滑权衡的选择。对我而言,如何表示一个"thing"的选择主要取决于我将如何使用这个结构。在C ++中,通常惯例是对仅包含数据项的使用struct,对带有方法的对象使用class,即使您可以合法地将方法放在struct上;我的习惯在Python中也类似,用dicttuple代替struct

对于坐标集,我会使用tuple,而不是点classdict(请注意,您可以使用tuple作为字典键,因此dict非常适合稀疏多维数组)。

如果我将迭代一系列"things",我更喜欢在迭代时解包tuple

for score,id,name in scoreAllTheThings():
    if score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(score,id,name)

...因为对象版本更加混乱难读:

for entry in scoreAllTheThings():
    if entry.score > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry.score,entry.id,entry.name)

更不用说dict了。

for entry in scoreAllTheThings():
    if entry['score'] > goodScoreThreshold:
        print "%6.3f #%6d %s"%(entry['score'],entry['id'],entry['name'])

如果某个东西被广泛使用,并且你发现自己在代码的多个地方进行类似的非平凡操作,那么通常值得将其转换成一个具有适当方法的类对象。

最后,如果我将要与非Python系统组件交换数据,我通常会将它们保存在dict中,因为这最适合JSON序列化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接