web-dev-qa-db-fra.com

comment diviser un itératif en morceaux de taille constante

Dupliquer possible:
Comment divisez-vous une liste en morceaux de taille égale en Python?

Je suis surpris de ne pas pouvoir trouver une fonction "batch" qui prendrait comme entrée un itérable et renverrait un itérable de iterables.

Par exemple:

for i in batch(range(0,10), 1): print i
[0]
[1]
...
[9]

ou:

for i in batch(range(0,10), 3): print i
[0,1,2]
[3,4,5]
[6,7,8]
[9]

Maintenant, j'ai écrit ce que je pensais être un générateur assez simple:

def batch(iterable, n = 1):
   current_batch = []
   for item in iterable:
       current_batch.append(item)
       if len(current_batch) == n:
           yield current_batch
           current_batch = []
   if current_batch:
       yield current_batch

Mais ce qui précède ne me donne pas ce à quoi je m'attendais:

for x in   batch(range(0,10),3): print x
[0]
[0, 1]
[0, 1, 2]
[3]
[3, 4]
[3, 4, 5]
[6]
[6, 7]
[6, 7, 8]
[9]

J'ai donc manqué quelque chose et cela montre probablement mon manque total de compréhension des générateurs de python. Quelqu'un voudrait me diriger dans la bonne direction?

[Edit: j'ai finalement réalisé que le comportement ci-dessus ne se produit que lorsque je lance ceci dans ipython plutôt que python lui-même]

55
mathieu

C'est probablement plus efficace (plus rapide)

def batch(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]

for x in batch(range(0, 10), 3):
    print x

Cela évite de construire de nouvelles listes.

71
Carl F.

FWIW, les recettes du module itertools fournit cet exemple:

def grouper(n, iterable, fillvalue=None):
    "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *args)

Cela fonctionne comme ceci:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
30
Raymond Hettinger

Comme d'autres l'ont noté, le code que vous avez donné fait exactement ce que vous voulez. Pour une autre approche utilisant itertools.islice, vous pourriez voir un exemple de la recette suivante:

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)
25
donkopotamus

Bizarre, semble bien fonctionner pour moi dans Python 2.x

>>> def batch(iterable, n = 1):
...    current_batch = []
...    for item in iterable:
...        current_batch.append(item)
...        if len(current_batch) == n:
...            yield current_batch
...            current_batch = []
...    if current_batch:
...        yield current_batch
...
>>> for x in batch(range(0, 10), 3):
...     print x
...
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
6
John Doe

Je viens de donner une réponse. Cependant, je pense maintenant que la meilleure solution pourrait être de ne pas écrire de nouvelles fonctions. More-itertools inclut de nombreux outils supplémentaires, et chunked en fait partie.

1
Yongwei Wu

C'est un extrait de code très court que je connais (pas ma création) qui n'utilise pas len et fonctionne à la fois avec Python 2 et 3 (pas ma création):

def chunks(iterable, size):
    from itertools import chain, islice
    iterator = iter(iterable)
    for first in iterator:
        yield list(chain([first], islice(iterator, size - 1)))
0
Yongwei Wu
def batch(iterable, n):
    iterable=iter(iterable)
    while True:
        chunk=[]
        for i in range(n):
            try:
                chunk.append(next(iterable))
            except StopIteration:
                yield chunk
                return
        yield chunk

list(batch(range(10), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
0
Atila Romero

Cela fonctionnerait pour tout itérable. 

from itertools import Zip_longest, filterfalse

def batch_iterable(iterable, batch_size=2): 
    args = [iter(iterable)] * batch_size 
    return (Tuple(filterfalse(lambda x: x is None, group)) for group in Zip_longest(fillvalue=None, *args))

Cela fonctionnerait comme ceci:

>>>list(batch_iterable(range(0,5)), 2)
[(0, 1), (2, 3), (4,)]

PS: Cela ne fonctionnerait pas si iterable avait des valeurs None.

0
Trideep Rath

C'est ce que j'utilise dans mon projet. Il gère les iterables ou les listes aussi efficacement que possible.

def chunker(iterable, size):
    if not hasattr(iterable, "__len__"):
        # generators don't have len, so fall back to slower
        # method that works with generators
        for chunk in chunker_gen(iterable, size):
            yield chunk
        return

    it = iter(iterable)
    for i in range(0, len(iterable), size):
        yield [k for k in islice(it, size)]


def chunker_gen(generator, size):
    iterator = iter(generator)
    for first in iterator:

        def chunk():
            yield first
            for more in islice(iterator, size - 1):
                yield more

        yield [k for k in chunk()]
0
Josh Smeaton

Voici une approche utilisant la fonction reduce.

Bon mot:

from functools import reduce
reduce(lambda cumulator,item: cumulator[-1].append(item) or cumulator if len(cumulator[-1]) < batch_size else cumulator + [[item]], input_array, [[]])

Ou version plus lisible:

from functools import reduce
def batch(input_list, batch_size):
  def reducer(cumulator, item):
    if len(cumulator[-1]) < batch_size:
      cumulator[-1].append(item)
      return cumulator
    else:
      cumulator.append([item])
    return cumulator
  return reduce(reducer, input_list, [[]])

Tester:

>>> batch([1,2,3,4,5,6,7], 3)
[[1, 2, 3], [4, 5, 6], [7]]
>>> batch(a, 8)
[[1, 2, 3, 4, 5, 6, 7]]
>>> batch([1,2,3,None,4], 3)
[[1, 2, 3], [None, 4]]
0
Lycha