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

1223 投票
14 回答
1389355 浏览
提问于 2025-04-11 21:01

在支持返回多个值的编程语言中,通常的做法是使用元组

选项:使用元组

考虑一个简单的例子:

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__

选项:使用数据类(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)

选项:使用列表

还有一个我之前忽略的建议,来自Bill the Lizard:

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

不过,我个人最不喜欢这种方法。我想我受到Haskell的影响,混合类型的列表总让我感到不舒服。在这个特定的例子中,列表并不是混合类型,但理论上是可以的。

以这种方式使用的列表在我看来并没有比元组更有优势。实际上,在Python中,列表和元组之间唯一的真正区别是,列表是可变的,而元组则不可变。

我个人倾向于沿用函数式编程的惯例:使用列表来处理相同类型的任意数量元素,而使用元组来处理固定数量的预定类型元素。

问题

经过这么长的引言,接下来就是不可避免的问题。你认为哪种方法最好?

14 个回答

242

很多回答都建议你需要返回某种集合,比如字典或者列表。其实你可以省略那些复杂的语法,直接把返回的值用逗号隔开写出来。注意:这样做实际上是返回了一个元组。

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

结果是:

True
False
265

对于小项目,我觉得用元组(tuples)最简单。当元组变得难以管理时(而不是之前),我才会开始把东西分组成更有逻辑的结构。不过,我觉得你提到的用字典和ReturnValue对象的做法不太对(或者说太简单了)。

返回一个包含键"y0""y1""y2"等的字典,并没有比元组更好的优势。返回一个ReturnValue实例,里面有属性.y0.y1.y2等,也没有比元组更好的优势。如果你想让事情变得更清晰,就需要开始给东西命名,而用元组也可以做到这一点:

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

size, type, dimensions = get_image_data(x)

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

693

在Python 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

撰写回答