web-dev-qa-db-fra.com

Concaténer des nuplets avec sum ()

De ce post j’ai appris que vous pouvez concaténer des n-uplets avec:

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

Ce qui semble joli Nice. Mais pourquoi ça marche? Et, est-ce optimal, ou y at-il quelque chose de itertools qui serait préférable à cette construction?

18
Stephen Rauch

l'opérateur d'addition concatène les n-uplets en python:

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

De la docstring de sum:

Retourne la somme d'une valeur 'de départ' (valeur par défaut: 0) plus une valeur itérable de Nombres

Cela signifie que sum ne commence pas par le premier élément de votre itérable, mais par une valeur initiale transmise par l'argument start=.

Par défaut, sum est utilisé avec numérique. La valeur de départ par défaut est donc 0. Donc, pour résumer une multiplicité de tuples, il faut commencer par un tuple vide. () est un tuple vide:

type(())
Out[36]: Tuple

Par conséquent, la concaténation de travail.

Selon la performance, voici une comparaison:

%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

Maintenant avec un t2 de taille 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

Donc, si votre liste de n-uplets est petite, cela ne vous dérange pas. Si sa taille est moyenne ou supérieure, vous devez utiliser itertools.

22
Boud

C’est intelligent et j’ai dû rire car l’aide interdit expressément les ficelles, mais ça marche

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.

Vous pouvez ajouter des nuplets pour obtenir un nouveau tuple plus grand. Et puisque vous avez donné un tuple comme valeur de départ, l’ajout fonctionne.

4
tdelaney

Juste pour compléter la réponse acceptée avec quelques points de repère supplémentaires:

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)

EDIT: le code est mis à jour pour utiliser les n-uplets. Et, comme indiqué dans les commentaires, les deux dernières options se trouvent maintenant dans un constructeur Tuple() et toutes les heures ont été mises à jour (pour des raisons de cohérence). Les options itertools.chain* sont toujours les plus rapides, mais maintenant la marge est réduite.

1
norok2

Cela fonctionne parce que l'addition est surchargée (sur les n-uplets) pour renvoyer le tuple concaténé:

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

C’est essentiellement ce que sum fait: vous donnez la valeur initiale d’un tuple vide, puis vous ajoutez les n-uplets à cela.

Cependant, c’est généralement une mauvaise idée, car l’ajout de n-uplets crée un nouveau tuple. Vous devez donc créer plusieurs n-uplets intermédiaires pour les copier dans le tuple concaténé:

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

C'est une implémentation qui a un comportement d'exécution quadratique. Ce comportement d'exécution quadratique peut être évité en évitant les n-uplets intermédiaires.

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

Utilisation d'expressions de générateur imbriquées:

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

Ou en utilisant une fonction de générateur:

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


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

Ou en utilisant itertools.chain.from_iterable :

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

Et si vous êtes intéressé par leurs performances (avec mon simple_benchmark package ):

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 bits, Windows 10 64 bits)

Ainsi, bien que l'approche sum soit très rapide si vous ne concatérez que quelques n-uplets, elle sera très lente si vous essayez de concaténer beaucoup de n-uplets. La plus rapide des approches testées pour de nombreux tuples est itertools.chain.from_iterable

0
MSeifert