一个函数返回多个值符合Pythonic风格吗?

88

在Python中,函数可以返回多个值。这里有一个人为的示例:

def divide(x, y):
    quotient = x/y
    remainder = x % y
    return quotient, remainder  

(q, r) = divide(22, 7)

这似乎非常有用,但它看起来也可能被滥用(“嗯...函数X已经计算出我们需要的中间值。让X也返回该值")。

什么时候应该划定界限并定义不同的方法?

9个回答

114

就您提供的例子而言,绝对是这样。

在Python中,元组是一等公民

有一个内置函数divmod()可以完美地做到这一点。

q, r = divmod(x, y) # ((x - x%y)/y, x%y) Invariant: div*y + mod == x

还有其他一些示例:zipenumeratedict.items

for i, e in enumerate([1, 3, 3]):
    print "index=%d, element=%s" % (i, e)

# reverse keys and values in a dictionary
d = dict((v, k) for k, v in adict.items()) # or 
d = dict(zip(adict.values(), adict.keys()))

顺便提一下,大多数情况下括号并不是必需的。引自Python官方文档

元组可以通过多种方式构造:

  • 使用一对空括号 () 表示空元组。
  • 使用逗号分隔的值表示含有一个元素的元组 (a, ) 或者 a,
  • 使用逗号分隔的值表示含有多个元素的元组 a, b, c 或者 (a, b, c)。
  • 调用内置函数tuple()创建元组,可从可迭代对象中获取元素,例如 tuple(iterable)。

函数应当只服务单一目的

因此,它们应该返回单一对象。在您的情况下,这个对象是一个元组。可以将元组视为临时复合数据结构。有些语言几乎每个函数都会返回多个值(Lisp中的列表)。

有时只需返回(x, y)而不是Point(x, y)就足够了。

命名元组

随着Python 2.6引入命名元组,许多情况下建议返回命名元组而不是普通元组。

>>> import collections
>>> Point = collections.namedtuple('Point', 'x y')
>>> x, y = Point(0, 1)
>>> p = Point(x, y)
>>> x, y, p
(0, 1, Point(x=0, y=1))
>>> p.x, p.y, p[0], p[1]
(0, 1, 0, 1)
>>> for i in p:
...   print(i)
...
0
1

1
能够像这样使用元组非常方便。有时候我真的很想在Java中拥有那种能力。 - MBCook
3
非常感谢你提到了命名元组。我一直在寻找这样的东西。 - vobject
对于返回值有限的情况,使用dict()可能更简单/更快(特别是如果函数用于REST API和JSON)。在我的系统上,def test_nt(): Point=namedtuple('Point', ['x', 'y']) p = Point(10.5, 11.5) json.dumps(p._asdict()) 需要819μs/loop,而def test_dict(): d = {'x': 10.5, 'y': 11.5} json.dumps(d) 需要15.9μs/loop.... 因此速度>50倍更快。 - comte
@comte:是的,Point(10.5, 11.5){'x':10.5, 'y':11.5}慢了6倍。绝对时间分别为635 ns +- 26 ns vs. 105 ns +- 4 ns。很少会有情况使二者成为应用程序的瓶颈 - 不要优化,除非您的分析工具指出需要优化。不要在函数级别创建类,除非您知道为什么需要它。如果您的API需要dict,请使用dict - 这与性能无关。 - jfs

27

首先,请注意Python允许以下写法(无需使用括号):

q, r = divide(22, 7)

关于您的问题,这并没有严格的规定。对于简单(通常是人为制造的)的例子来说,一个给定的函数可能总是有一个单一的目的,从而产生一个单一的值。然而,在使用Python进行实际应用时,您很快就会遇到许多需要返回多个值的情况,并且这能使代码更清晰。

因此,我认为应该按照逻辑操作,不要试图遵守人为惯例。Python支持多个返回值,因此在合适的情况下可以使用它们。


4
逗号可以创建元组,因此表达式q, r是一个元组。 - jfs
Vinko,一个例子是标准库中的tempfile.mkstemp(),它返回一个元组,其中包含文件句柄和绝对路径。JFS,是的,你说得对。 - Jason Etheridge
但这确实是一个单一的目的...您可以拥有单一的目的和多个返回值。 - Vinko Vrsalovic
如果其他编程语言可以有多个返回类型,我就不会被限制于引用和“out”参数了。 - orip
返回多个值是Python中最伟大的特性之一! - Martin Beckett

13

您提供的例子实际上是Python内置函数,称为divmod。因此,某个时刻的某人认为它足够符合Python的风格,因此将其包含在核心功能中。

对我而言,如果它使代码更简洁,那就是Pythonic的。比较这两种代码块:

seconds = 1234
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)

seconds = 1234
minutes = seconds / 60
seconds = seconds % 60
hours = minutes / 60
minutes = minutes % 60

4

是的,返回多个值(即元组)绝对符合Python的风格。正如其他人所指出的那样,在Python标准库和备受尊重的Python项目中有大量这样的示例。还有两点需要注意:

  1. Returning multiple values is sometimes very, very useful. Take, for example, a method that optionally handles an event (returning some value in doing so) and also returns success or failure. This might arise in a chain of responsibility pattern. In other cases, you want to return multiple, closely linked pieces of data---as in the example given. In this setting, returning multiple values is akin to returning a single instance of an anonymous class with several member variables.
  2. Python's handling of method arguments necessitates the ability to directly return multiple values. In C++, for example, method arguments can be passed by reference, so you can assign output values to them, in addition to the formal return value. In Python, arguments are passed "by reference" (but in the sense of Java, not C++). You can't assign new values to method arguments and have it reflected outside method scope. For example:

    // C++
    void test(int& arg)
    {
        arg = 1;
    }
    
    int foo = 0;
    test(foo); // foo is now 1!
    

    Compare with:

    # Python
    def test(arg):
        arg = 1
    
    foo = 0
    test(foo) # foo is still 0
    

一个可选处理事件并返回成功或失败的方法。那就是异常处理的用途。 - Nathan
异常通常被广泛认为是仅用于“异常”情况,而不是仅仅表示成功或失败。可以搜索“错误码 vs 异常”来了解相关讨论。 - zweiterlinde

1

OT: RSRE的Algol68有一个奇特的"/:="运算符。例如。

INT quotient:=355, remainder;
remainder := (quotient /:= 113);

给定商为3,余数为16。

注意:通常情况下,“(x/:=y)”的值会被丢弃,因为商“x”是通过引用赋值的,但在RSRE的情况下,返回的值是余数。

参见整数算术- Algol68


1

这绝对是Pythonic的。你可以从函数中返回多个值,而不需要在像C语言中那样为每种类型的组合定义一个结构体。

然而,如果你要从单个函数中返回10个以上的值,你应该认真考虑将它们捆绑在一个类中,因为此时它会变得难以控制。


1

0

我对Python还比较新,但元组技巧对我来说非常符合Python的风格。然而,我有另一个想法可能能提高代码可读性。使用字典可以通过名称而不是位置访问不同的值。例如:

def divide(x, y):
    return {'quotient': x/y, 'remainder':x%y }

answer = divide(22, 7)
print answer['quotient']
print answer['remainder']

2
不要这样做。你不能在一行代码中分配结果,这很笨重。(除非你想存储结果,在这种情况下最好将其放入一个方法中。)如果你的目的是用字典自我记录返回值,那么a)给函数一个合理的解释性名称(比如“divmod”),b)将其余的说明放入文档字符串中(这是Pythonic的做法);如果该文档字符串需要超过两行,那就意味着该函数正在进行主题重载,可能是个坏主意。 - smci

0

对于像 divmod 这样的简单函数,使用元组返回多个值是可以的。如果这样做可以使代码更易读,那么就符合 Pythonic 的风格。

如果返回值开始变得混乱,请检查函数是否做了太多的事情,并在必要时进行拆分。如果一个大的元组被用作对象,请将其转换为对象。此外,考虑使用named tuples,它将成为 Python 2.6 标准库的一部分。


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