使用sum()函数连接元组

26

这篇帖子中我学到,你可以使用sum()函数来连接元组:

>>> tuples = (('hello',), ('these', 'are'), ('my', 'tuples!'))
>>> sum(tuples, ())
('hello', 'these', 'are', 'my', 'tuples!')

看起来相当不错。但是为什么这行得通呢?而且,这是否是最优的实现方式,或者在 itertools 中有更好的替代方案呢?


2
为什么它不能工作呢?它只是将元组相加,但效率并不高。看一下itertools.chain。例如,tuple(chain(*tuples)) - PM 2Ring
1
@PM2Ring。避免像这样使用chain,因为它比sum更低效(除非元组的集合非常小)。改用chain.from_iterable - ekhumoro
1
@ekhumoro 哎呀!是的,chain.from_iterable 更好。正如 Boud 的回答所示,对于小元组集合,它实际上比 sum 函数更慢。 - PM 2Ring
5个回答

31

在 Python 中,加法运算符用于连接元组(tuples):

('a', 'b')+('c', 'd')
Out[34]: ('a', 'b', 'c', 'd')

sum 的文档字符串中可以得知:

返回一个数字迭代器的 'start' 值(默认为 0)的总和。

这意味着 sum 不是从可迭代对象的第一个元素开始计算,而是从传递给 start= 参数的初始值开始计算。

默认情况下,sum 用于数字,因此默认的起始值为 0。因此,对元组的可迭代对象求和需要以空元组开始。() 是一个空元组。

type(())
Out[36]: tuple

因此,工作串联。

就性能而言,这里有一个比较:

%timeit sum(tuples, ())
The slowest run took 9.40 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 285 ns per loop


%timeit tuple(it.chain.from_iterable(tuples))
The slowest run took 5.00 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 625 ns per loop

现在t2的大小为10000:

%timeit sum(t2, ())
10 loops, best of 3: 188 ms per loop

%timeit tuple(it.chain.from_iterable(t2))
1000 loops, best of 3: 526 µs per loop

所以,如果你的元组列表很小,那就不必费心了。但如果它是中等大小或更大,则应使用itertools


有趣的时间。你用的是哪个Python版本? - PM 2Ring
@PM2Ring 3.5 64位 - Zeugma
1
请参考IPython中的%timeit文档。 - Zeugma

5
它之所以有效是因为在元组上重载了加法运算符,返回连接后的元组:
>>> () + ('hello',) + ('these', 'are') + ('my', 'tuples!')
('hello', 'these', 'are', 'my', 'tuples!')

这基本上就是sum正在做的事情,你给出一个空元组的初始值,然后将元组添加到其中。

然而,这通常是一个不好的主意,因为元组的相加会创建一个新的元组,所以你需要创建多个中间元组,然后将它们复制到连接的元组中:

()
('hello',)
('hello', 'these', 'are')
('hello', 'these', 'are', 'my', 'tuples!')

这是一种具有二次运行时行为的实现。可以通过避免中间元组来避免这种二次运行时行为。

>>> tuples = (('hello',), ('these', 'are'), ('my', 'tuples!'))

使用嵌套生成器表达式:

>>> tuple(tuple_item for tup in tuples for tuple_item in tup)
('hello', 'these', 'are', 'my', 'tuples!')

或者使用生成器函数:

def flatten(it):
    for seq in it:
        for item in seq:
            yield item


>>> tuple(flatten(tuples))
('hello', 'these', 'are', 'my', 'tuples!')

或者使用itertools.chain.from_iterable

>>> import itertools
>>> tuple(itertools.chain.from_iterable(tuples))
('hello', 'these', 'are', 'my', 'tuples!')

如果您对这些表现感兴趣(使用我的simple_benchmark包):

import itertools
import simple_benchmark

def flatten(it):
    for seq in it:
        for item in seq:
            yield item

def sum_approach(tuples):
    return sum(tuples, ())

def generator_expression_approach(tuples):
    return tuple(tuple_item for tup in tuples for tuple_item in tup)

def generator_function_approach(tuples):
    return tuple(flatten(tuples))

def itertools_approach(tuples):
    return tuple(itertools.chain.from_iterable(tuples))

funcs = [sum_approach, generator_expression_approach, generator_function_approach, itertools_approach]
arguments = {(2**i): tuple((1,) for i in range(1, 2**i)) for i in range(1, 13)}
b = simple_benchmark.benchmark(funcs, arguments, argument_name='number of tuples to concatenate')

b.plot()

enter image description here

如果你只连接少量元组,那么使用sum方法会非常快,但是如果你尝试连接大量元组,它将变得非常慢。在连接多个元组时,已测试的最快方法是itertools.chain.from_iterable

(Python 3.7.2 64位,Windows 10 64位)


3

这很聪明,我不得不笑,因为帮助明确禁止字符串,但它可以工作。

sum(...)
    sum(iterable[, start]) -> value
    
    Return the sum of an iterable of numbers (NOT strings) plus the value
    of parameter 'start' (which defaults to 0).  When the iterable is
    empty, return start.

您可以添加元组以获得一个新的、更大的元组。由于您提供了一个元组作为起始值,所以加法运算可行。


1
在这个例子中,sum并没有对字符串求和:在输入中分开的两个字符串在这里没有被连接起来。(例如,无法使用sumhelloworld变成helloworld。) - ShreevatsaR
1
在我看来,Python 的做法很愚蠢。Sum 应该能够对任何支持 + 运算符的对象进行求和,包括字符串。为了追求性能和良好的规范,而明确禁止字符串这种情况,这种设计并不好。尤其是 Python 中还有许多其他不被禁止的反模式存在。 - mike3996
@ShreevatsaR 我非常清楚那一点。帮助文件提到了字符串,但我接着说这实际上是在添加元组。我只是觉得这很有趣,认为人们可以阅读。 - tdelaney
@progo - 我不确定为什么被禁止,但我同意,sum应该做加法所做的事情。也许是为了捕捉将字符串误认为整数的常见错误。但是... - tdelaney
关于字符串部分,请参见https://dev59.com/_XA75IYBdhLWcg3wAD_x。这主要是为了效率。 - matsjoyce
1
“help”明确禁止使用字符串,但它却能正常工作。这可能会被误解为“它也适用于字符串”,但事实并非如此。此外,这个回答怎么解决问题呢?引用的帮助文件甚至没有提到值或起始位置可以是除了数字以外的任何东西,更不用说元组了。 - tobias_k

1

补充一下已被接收的答案,提供更多基准测试数据:

import functools, operator, itertools
import numpy as np
N = 10000
M = 2

ll = tuple(tuple(x) for x in np.random.random((N, M)).tolist())

%timeit functools.reduce(operator.add, ll)
# 407 ms ± 5.63 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit functools.reduce(lambda x, y: x + y, ll)
# 425 ms ± 7.16 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit sum(ll, ())
# 426 ms ± 14.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit tuple(itertools.chain(*ll))
# 601 µs ± 5.43 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit tuple(itertools.chain.from_iterable(ll))
# 546 µs ± 25.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

编辑:代码已更新以实际使用元组。根据评论,最后两个选项现在位于tuple()构造函数内,并且所有时间都已更新(为了一致性)。itertools.chain*选项仍然是最快的,但现在边距已经缩小。


你最后两个时间不具有代表性。itertools.chainitertools.chain.from_iterable返回迭代器。为了公平计时,你需要使用 tuple(itertools.chain...) 进行迭代。 - MSeifert

0

第二个参数start,在你放置()的地方,是要添加到的起始对象,对于数字加法,默认为0

这里是sum的一个示例实现(我的期望):

def sum(iterable, /, start=0):
    for element in iterable:
        start += element
    return start

例子:

>>> sum([1, 2, 3])
6
>>> tuples = (('hello',), ('these', 'are'), ('my', 'tuples!'))
>>> sum(tuples)
TypeError: unsupported operand type(s) for +=: 'int' and 'tuple'
>>> sum(tuples, ())
('hello', 'these', 'are', 'my', 'tuples!')
>>> 

由于元组连接支持使用+,因此它可以正常工作。

实际上,这被翻译为:

>>> () + ('hello',) + ('these', 'are') + ('my', 'tuples!')
('hello', 'these', 'are', 'my', 'tuples!')
>>> 

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