Объединение кортежей с использованием sum()

Из этого сообщения я узнал, что вы можете связать кортежи с помощью:

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

Что выглядит довольно красиво. Но почему это работает? И, является ли это оптимальным, или есть что-то от itertools которые были бы предпочтительнее этой конструкции?

Ответ 1

оператор добавления объединяет кортежи в python:

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

Из строку документации на sum:

Возвращает сумму значения "start" (по умолчанию: 0) плюс итерабельность чисел

Это означает, что sum не начинается с первого элемента вашего итерабельного, а скорее с начальным значением, которое передается через start= argument.

По умолчанию 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.

Ответ 2

Это умно, и мне пришлось смеяться, потому что помощь прямо запрещает строки, но это работает

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.

Вы можете добавить кортежи, чтобы получить новый, более крупный кортеж. И так как вы дали кортеж в качестве начального значения, добавление работает.

Ответ 3

Просто чтобы дополнить принятый ответ еще несколькими критериями:

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

ll = 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 list(itertools.chain(*ll))
# 601 µs ± 5.43 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

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

РЕДАКТИРОВАТЬ: согласно комментариям, последние две опции теперь находятся внутри конструкторов list(), и все времена были обновлены (для согласованности). itertools.chain* по-прежнему самые быстрые, но теперь маржа уменьшена.

Ответ 4

Это работает, потому что сложение перегружено (для кортежей) для возврата сцепленного кортежа:

>>> () + ('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

(Python 3.7.2 64-битная, Windows 10 64-битная)

Таким образом, хотя sum подход очень быстр, если вы объединяете только несколько кортежей, он будет очень медленным, если вы попытаетесь объединить множество кортежей. Самый быстрый из протестированных подходов для многих кортежей - itertools.chain.from_iterable