【问题标题】:Best way to return multiple values from a function? [closed]从函数返回多个值的最佳方法? [关闭]
【发布时间】:2010-09-26 04:11:51
【问题描述】:

以支持它的语言返回多个值的规范方法通常是tupling

选项:使用元组

考虑这个简单的例子:

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 Reference Manual

__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 而受到污染,但混合类型列表的想法一直让我感到不舒服。在这个特定示例中,列表是 -not-mixed 类型,但可以想象它可能是。

据我所知,以这种方式使用的列表实际上对元组没有任何好处。 Python 中列表和元组之间唯一真正的区别是列表是mutable,而元组不是。

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

问题

在冗长的序言之后,不可避免的问题来了。哪种方法(您认为)最好?

【问题讨论】:

  • 在您的优秀示例中,您使用变量 y3,但除非将 y3 声明为全局变量,否则这将产生 NameError: global name 'y3' is not defined,也许只使用 3
  • 许多带有好答案的好问题都被关闭了,因为出现了“意见”关键字。您可能会争辩说整个 SO 都是基于意见的,但它是由事实、参考资料和特定专业知识得出的意见。仅仅因为有人问“你认为哪个最好”并不意味着他们要求从现实世界的事实、参考资料和特定专业知识中抽象出来的个人意见。他们几乎可以肯定地要求正是这种意见,这种意见完全基于并记录了该人用来形成意见的事实、参考资料和具体专业知识。
  • @hetepeperfan 不需要更改 3,也不需要在全局中定义 y3,您也可以使用本地名称 y3,也可以完成相同的工作。

标签: python coding-style return return-value


【解决方案1】:

Named tuples 为此在 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

【讨论】:

  • 这只是正确的答案,因为它是 OP 没有考虑的唯一规范结构,并且因为它解决了他管理长元组的问题。应标记为已接受。
  • 嗯,namedtuple 的设计原理是为 mass 结果(元组的长列表,例如 DB 查询的结果)占用更小的内存。对于单个项目(如果所讨论的函数不经常调用),字典和类也很好。但在这种情况下,namedtuples 也是一个不错/更好的解决方案。
  • @wom:不要这样做。 Python 不努力使 namedtuple 定义唯一化(每次调用都会创建一个新的),创建 namedtuple 类在 CPU 和内存方面都相对昂贵,并且所有类定义本质上都涉及循环引用(所以在 CPython 上,你是等待循环 GC 运行以释放它们)。这也使得pickle 类无法使用(因此,在大多数情况下,无法使用multiprocessing 的实例)。在我的 3.6.4 x64 上每次创建该类都会消耗大约 0.337 毫秒,并且占用不到 1 KB 的内存,从而导致任何实例节省。
  • 我会注意,Python 3.7 improved the speed of creating new namedtuple classes; the CPU costs drop by roughly a factor of 4x,但它们仍然比创建实例的成本高出大约 1000 倍,并且每个类的内存成本仍然很高(我在上一条关于类“低于 1 KB”的评论中是错误的,_source其本身通常为 1.5 KB;_source 在 3.7 中被删除,因此它可能更接近于最初声称的每个类创建不到 1 KB)。
  • @SergeStroobandt - 这是标准库的一部分,不是内置的。您不必担心它可能不会安装在 Python >= 2.6 的另一个系统上。还是只是反对多出一行代码?
【解决方案2】:

对于小型项目,我发现使用元组最容易。当这变得难以管理时(而不是之前),我开始将事物分组到逻辑结构中,但是我认为您建议使用字典和 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) 获得的一样。

【讨论】:

  • 问题 - size, type, dimensions = getImageData(x)(size, type, dimensions) = getImageData(x) 之间有什么区别吗?即,将元组赋值的左侧包装起来有什么不同吗?
  • @Reb.Cabin 没有区别。元组由逗号标识,括号的使用只是将事物组合在一起。例如(1) 是一个 int 而(1,)1, 是一个元组。
  • 关于“返回带有键 y0、y1、y2 等的字典与元组相比没有任何优势”:字典的优势在于您可以在不破坏现有代码的情况下向返回的字典添加字段。
  • 关于“返回带有键 y0、y1、y2 等的字典与元组相比没有任何优势”:当您根据其名称访问数据时,它也更具可读性且不易出错位置。
【解决方案3】:

许多答案表明您需要返回某种集合,例如字典或列表。您可以省略额外的语法,只写出返回值,以逗号分隔。注意:这在技术上返回一个元组。

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

给予:

True
False

【讨论】:

  • 您仍在返回一个集合。它是一个元组。我更喜欢括号来使其更明确。试试这个:type(f()) 返回<class 'tuple'>
  • @Igor:没有理由明确说明tuple 方面;返回tuple 并不重要,这是返回多个值期间的习惯用法。同样的原因,您使用交换习语省略括号,x, y = y, x,多重初始化x, y = 0, 1 等;当然,它使tuples 在幕后,但没有理由明确表示,因为tuples 根本不是重点。 Python 教程 introduces multiple assignment 早在它触及 tuples 之前。
  • @ShadowRanger 在= 右侧用逗号分隔的任何值序列都是 Python 中的元组,它们周围有或没有括号。所以这里实际上没有显式或隐式。 a,b,c 和 (a,b,c) 一样是一个元组。当你返回这样的值时,也没有“幕后”的元组,因为它只是一个简单的元组。 OP 已经提到了元组,所以他提到的内容和这个答案显示的内容实际上没有区别。无
  • 这是问题中建议的第一个选项
  • @endolith 这家伙问了两次问题(“我如何返回多个值?”和“you 如何返回多个值?”)由这个回答回答。问题的文本有时会发生变化。这是一个基于意见的问题。
【解决方案4】:

我为字典投票。

我发现如果我创建一个返回超过 2-3 个变量的函数,我会将它们折叠到字典中。否则我往往会忘记返回的顺序和内容。

此外,引入“特殊”结构会使您的代码更难遵循。 (其他人将不得不搜索代码以找出它是什么)

如果您担心类型查找,请使用描述性字典键,例如“x 值列表”。

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

【讨论】:

  • 经过多年的编程,我倾向于数据和函数的结构。功能优先,您可以随时根据需要进行重构。
  • 如何在不多次调用函数的情况下获取字典中的值?例如,如果我想在不同的函数中使用 y1 和 y3?
  • 将结果分配给单独的变量。 result = g(x); other_function(result)
  • @monkut 是的。这种方式还允许将结果传递给多个函数,这些函数从结果中获取不同的参数,而不必每次都专门引用结果的特定部分。
【解决方案5】:

另一种选择是使用生成器:

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

尽管恕我直言,元组通常是最好的,除非返回的值是封装在类中的候选值。

【讨论】:

  • 这似乎是最简洁的解决方案,并且语法简洁。这有什么缺点吗?如果您不使用所有回报,是否有“未花费”的收益等着伤害您?
  • 这可能很“干净”,但看起来一点也不直观。以前从未遇到过这种模式的人怎么会知道进行自动元组解包会触发每个yield
  • @CoreDumpError,生成器就是……生成器。 def f(x): …; yield b; yield a; yield r(g for g in [b, a, r]) 之间没有外部区别,两者都可以轻松转换为列表或元组,因此将支持元组解包。元组生成器形式遵循函数式方法,而函数形式是命令式的,将允许流控制和变量赋值。
【解决方案6】:

我更喜欢:

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

似乎其他一切都只是做同样事情的额外代码。

【讨论】:

  • 元组更容易解包: y0, y1, y2 = g() 用你必须做的字典: result = g() y0, y1, y2 = result.get('y0') , result.get('y1'), result.get('y2') 这有点难看。每个解决方案都有其“优点”和“缺点”。
【解决方案7】:

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

仅当分组对象并不总是相同时,我才使用字典作为返回值。考虑可选的电子邮件标题。

对于其余的情况,如果分组对象在组内具有内在含义,或者需要一个具有自己方法的成熟对象,我会使用一个类。

【讨论】:

    【解决方案8】:
    >>> def func():
    ...    return [1,2,3]
    ...
    >>> a,b,c = func()
    >>> a
    1
    >>> b
    2
    >>> c
    3
    

    【讨论】:

    • @edouard 不,它没有,它返回一个元组而不是一个列表。
    • 解构是 在我看来返回列表的参数
    【解决方案9】:

    通常,“特殊结构”实际上是对象的合理当前状态,具有自己的方法。

    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
    

    我喜欢尽可能找到匿名结构的名称。有意义的名字让事情更清楚。

    【讨论】:

      【解决方案10】:

      Python 的元组、字典和对象为程序员提供了在小型数据结构(“事物”)的形式性和便利性之间的平滑折衷。对我来说,如何表示事物的选择主要取决于我将如何使用该结构。在 C++ 中,将struct 用于纯数据项,将class 用于具有方法的对象是一种常见的约定,即使您可以合法地将方法放在struct 上;我的习惯在 Python 中类似,用dicttuple 代替struct

      对于坐标集,我将使用 tuple 而不是点 classdict(请注意,您可以使用 tuple 作为字典键,所以 dicts 非常有用稀疏多维数组)。

      如果我要迭代一个列表,我更喜欢在迭代中解压tuples:

      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 序列化。

      【讨论】:

        【解决方案11】:

        +1 关于 S.Lott 对命名容器类的建议。

        对于 Python 2.6 及更高版本,named tuple 提供了一种轻松创建这些容器类的有用方法,其结果是“轻量级且不需要比常规元组更多的内存”。

        【讨论】:

          【解决方案12】:

          “最佳”是一个部分主观的决定。在可以接受不可变的一般情况下,将元组用于小返回集。当不需要可变性时,元组总是比列表更可取。

          对于更复杂的返回值,或者对于形式很有价值的情况(即高价值代码),命名元组更好。对于最复杂的情​​况,对象通常是最好的。然而,真正重要的是情况。如果返回一个对象是有意义的,因为这是您在函数末尾自然拥有的(例如工厂模式),那么返回该对象。

          正如智者所说:

          过早的优化是万恶之源(或至少大部分 它)在编程中。

          【讨论】:

          【解决方案13】:

          在像 Python 这样的语言中,我通常会使用字典,因为它比创建新类所需的开销更少。

          但是,如果我发现自己经常返回相同的变量集,那么这可能涉及到一个新的类,我将把它排除在外。

          【讨论】:

            【解决方案14】:

            我会使用 dict 从函数中传递和返回值:

            使用form中定义的变量形式。

            form = {
                'level': 0,
                'points': 0,
                'game': {
                    'name': ''
                }
            }
            
            
            def test(form):
                form['game']['name'] = 'My game!'
                form['level'] = 2
            
                return form
            
            >>> print(test(form))
            {u'game': {u'name': u'My game!'}, u'points': 0, u'level': 2}
            

            这对我和处理单元来说是最有效的方式。

            你只需要传入一个指针并返回一个指针。

            每当您对代码进行更改时,您都不必更改函数的(数千个)参数。

            【讨论】:

            • 字典是可变的。如果您将 dict 传递给函数并且该函数编辑该 dict,则更改将反映在该函数范围之外。让函数在最后返回dict可能意味着该函数没有副作用,因此不应返回该值,明确test将直接修改该值。将此与不返回值的dict.update 进行比较。
            • @sleblanc “让函数在最后返回 dict 可能意味着该函数没有副作用”。这并不意味着因为,正如你所说, dict 是可变的。但是,返回 form 不会影响可读性和性能。如果您可能需要重新格式化 form,返回它 [form] 确实确保返回最后一个 form,因为您不会在任何地方跟踪表单更改。
            猜你喜欢
            • 2010-09-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-02-03
            • 2017-05-22
            相关资源
            最近更新 更多