Question

J'ai un script Python qui prend en entrée une liste d'entiers que je dois utiliser avec quatre entiers à la fois. Malheureusement, je n'ai pas le contrôle de l'entrée, sinon je la ferais passer sous forme de liste de tuples à quatre éléments. Actuellement, je revis cela de cette façon:

for i in xrange(0, len(ints), 4):
    # dummy op for example code
    foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]

Cela ressemble pourtant beaucoup à "C-think", ce qui me laisse supposer qu'il existe un moyen plus pythonique de gérer cette situation. La liste est supprimée après itération, elle n'a donc pas besoin d'être conservée. Peut-être que quelque chose comme ça serait mieux?

while ints:
    foo += ints[0] * ints[1] + ints[2] * ints[3]
    ints[0:4] = []

Vous n'avez toujours pas "l'impression" & droit, cependant. : - /

Question connexe: Comment vous divisez une liste en morceaux de taille uniforme en Python?

Était-ce utile?

La solution

Modifié à partir de la section recettes de itertools docs:

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

Exemple
En pseudo-code, gardez l’exemple en lacet.

grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'

Remarque: sous Python 2, utilisez izip_longest au lieu de zip_longest .

Autres conseils

def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))
# (in python 2 use xrange() instead of range() to avoid allocating a list)

Simple. Facile. Vite. Fonctionne avec n'importe quelle séquence:

text = "I am a very, very helpful text"

for group in chunker(text, 7):
   print repr(group),
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'

print '|'.join(chunker(text, 10))
# I am a ver|y, very he|lpful text

animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']

for group in chunker(animals, 3):
    print group
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']

Je suis fan de

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size
import itertools
def chunks(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
    foo += x1 + x2 + x3 + x4

for chunk in chunks(ints,4):
    foo += sum(chunk)

Autre moyen:

import itertools
def chunks2(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
    foo += x1 + x2 + x3 + x4
from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)

J'avais besoin d'une solution qui fonctionnerait également avec des ensembles et des générateurs. Je ne pouvais rien trouver de très court et joli, mais au moins c'est assez lisible.

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

Liste:

>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Définir:

>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Générateur:

>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Semblable à d'autres propositions, mais pas exactement identique, j'aime bien le faire, car c'est simple et facile à lire:

it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
    print chunk

>>> (1, 2, 3, 4)
>>> (5, 6, 7, 8)

De cette façon, vous n’obtiendrez pas le dernier bloc partiel. Si vous souhaitez obtenir (9, Aucun, Aucun, Aucun) comme dernier bloc, utilisez simplement izip_longest à partir de itertools .

La solution idéale à ce problème fonctionne avec des itérateurs (pas seulement des séquences). Il devrait également être rapide.

C’est la solution fournie par la documentation pour itertools:

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

En utilisant le % timeit d'ipython sur mon MacBook Air, je reçois 47,5 us par boucle.

Cependant, cela ne fonctionne vraiment pas pour moi car les résultats sont remplis pour former des groupes de taille égale. Une solution sans rembourrage est légèrement plus compliquée. La solution la plus naïve pourrait être:

def grouper(size, iterable):
    i = iter(iterable)
    while True:
        out = []
        try:
            for _ in range(size):
                out.append(i.next())
        except StopIteration:
            yield out
            break

        yield out

Simple, mais assez lent: 693 us par boucle

La meilleure solution que je puisse trouver utilise islice pour la boucle interne:

def grouper(size, iterable):
    it = iter(iterable)
    while True:
        group = tuple(itertools.islice(it, None, size))
        if not group:
            break
        yield group

Avec le même jeu de données, je reçois 305 us par boucle.

Impossible d'obtenir une solution pure plus rapidement que cela, je présente la solution suivante avec une mise en garde importante: si vos données d'entrée contiennent des instances de filldata , vous risquez d'obtenir une réponse erronée.

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    for i in itertools.izip_longest(fillvalue=fillvalue, *args):
        if tuple(i)[-1] == fillvalue:
            yield tuple(v for v in i if v != fillvalue)
        else:
            yield i

Je n'aime vraiment pas cette réponse, mais elle est nettement plus rapide. 124 us par boucle

Puisque personne n'en a encore parlé, voici une solution zip () :

>>> def chunker(iterable, chunksize):
...     return zip(*[iter(iterable)]*chunksize)

Cela ne fonctionne que si la longueur de votre séquence est toujours divisible par la taille du bloc ou si vous ne vous souciez pas du dernier morceau s'il ne l'est pas.

Exemple:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

Ou utilisez itertools.izip pour renvoyer un itérateur au lieu d'une liste:

>>> from itertools import izip
>>> def chunker(iterable, chunksize):
...     return izip(*[iter(iterable)]*chunksize)

Le rembourrage peut être corrigé en utilisant La réponse de @ ??O????? :

>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
...     it   = chain(iterable, repeat(fillvalue, chunksize-1))
...     args = [it] * chunksize
...     return izip(*args)

L'utilisation de map () au lieu de zip () corrige le problème de remplissage dans la réponse de J.F. Sebastian:

>>> def chunker(iterable, chunksize):
...   return map(None,*[iter(iterable)]*chunksize)

Exemple:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

Si vous ne craignez pas l’utilisation d’un logiciel externe, vous pouvez utiliser iteration_utilities.grouper à partir de iteration_utilties 1 . Il supporte tous les iterables (pas seulement les séquences):

from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
    print(group)

qui imprime:

(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)

Si la longueur n'est pas un multiple de la taille de groupe, elle prend également en charge le remplissage (dernier groupe incomplet) ou la troncature (suppression du dernier groupe incomplet) du dernier:

from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)

for group in grouper(seq, 4, fillvalue=None):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)

for group in grouper(seq, 4, truncate=True):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)

1 Avertissement: je suis l'auteur de ce paquet.

Si la liste est longue, la méthode la plus performante consiste à utiliser un générateur:

def get_chunk(iterable, chunk_size):
    result = []
    for item in iterable:
        result.append(item)
        if len(result) == chunk_size:
            yield tuple(result)
            result = []
    if len(result) > 0:
        yield tuple(result)

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

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)

L’utilisation de petites fonctions et de choses ne m’intéresse pas vraiment; Je préfère simplement utiliser des tranches:

data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
    ...

Une autre approche consisterait à utiliser la forme à deux arguments de iter :

from itertools import islice

def group(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

Cela peut être facilement adapté pour utiliser un remplissage (cela ressemble à la réponse de Markus Jarderot ):

from itertools import islice, chain, repeat

def group_pad(it, size, pad=None):
    it = chain(iter(it), repeat(pad))
    return iter(lambda: tuple(islice(it, size)), (pad,) * size)

Ceux-ci peuvent même être combinés pour un remplissage optionnel:

_no_pad = object()
def group(it, size, pad=_no_pad):
    if pad == _no_pad:
        it = iter(it)
        sentinel = ()
    else:
        it = chain(iter(it), repeat(pad))
        sentinel = (pad,) * size
    return iter(lambda: tuple(islice(it, size)), sentinel)

Avec NumPy, rien de plus simple:

ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
    print(int1, int2)

sortie:

1 2
3 4
5 6
7 8

À moins que quelque chose ne me manque, la solution simple suivante avec les expressions du générateur n'a pas été mentionnée. Cela suppose que la taille et le nombre de morceaux sont connus (ce qui est souvent le cas), et qu'aucun remplissage n'est requis:

def chunks(it, n, m):
    """Make an iterator over m first chunks of size n.
    """
    it = iter(it)
    # Chunks are presented as tuples.
    return (tuple(next(it) for _ in range(n)) for _ in range(m))

Dans votre deuxième méthode, je passerais au groupe de 4 suivant en procédant ainsi:

ints = ints[4:]

Cependant, je n’ai fait aucune mesure de la performance, je ne sais donc pas laquelle pourrait être plus efficace.

Cela dit, je choisirais généralement la première méthode. Ce n’est pas beau, mais c’est souvent la conséquence d’une interface avec le monde extérieur.

Encore une autre réponse, dont les avantages sont:

1) Facilement compréhensible
2) Fonctionne sur n'importe quelle séquence, pas seulement des séquences (certaines des réponses ci-dessus vont s'étouffer avec les poignées de fichier)

3) Ne charge pas le bloc en mémoire en une fois

4) Ne fait pas une longue liste de références au même itérateur en mémoire
5) Pas de remplissage des valeurs de remplissage à la fin de la liste

Cela étant dit, je ne l'ai pas chronométré, donc cela pourrait être plus lent que certaines des méthodes les plus intelligentes, et certains des avantages pourraient ne pas être pertinents compte tenu du cas d'utilisation.

def chunkiter(iterable, size):
  def inneriter(first, iterator, size):
    yield first
    for _ in xrange(size - 1): 
      yield iterator.next()
  it = iter(iterable)
  while True:
    yield inneriter(it.next(), it, size)

In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:                                                
          for c in ii:
            print c,
          print ''
        ...:     
        a b c 
        d e f 
        g h 

Mise à jour:
Quelques inconvénients dus au fait que les boucles interne et externe extraient des valeurs du même itérateur:
1) continue ne fonctionne pas comme prévu dans la boucle externe - il continue simplement à l'élément suivant au lieu de sauter un bloc. Cependant, cela ne semble pas être un problème car il n’ya rien à tester dans la boucle externe.
2) la pause ne fonctionne pas comme prévu dans la boucle interne - le contrôle reviendra dans la boucle interne avec le prochain élément de l'itérateur. Pour ignorer des morceaux entiers, enveloppez l’itérateur interne (ii ci-dessus) dans un tuple, par ex. pour c dans le tuple (ii) , ou définissez un indicateur et épuisez l'itérateur.

def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

Vous pouvez utiliser la partition ou chunks fonctionne de funcy bibliothèque:

from funcy import partition

for a, b, c, d in partition(4, ints):
    foo += a * b * c * d

Ces fonctions ont également les versions d’itérateur ipartition et ichunks , ce qui sera plus efficace dans ce cas.

Vous pouvez également consulter leur mise en œuvre .

Pour éviter toute conversion vers une liste importer itertools et:

>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
...     list(g)

Produit:

... 
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>> 

J'ai vérifié groupby et il ne convertit pas en liste ni n'utilise len . Je pense donc que cela retardera la résolution de chaque valeur jusqu'à son utilisation effective. Malheureusement, aucune des réponses disponibles (pour le moment) ne semblait offrir cette variation.

Évidemment, si vous devez gérer chaque élément à tour de rôle imbriquer une boucle for sur g:

for k,g in itertools.groupby(xrange(35), lambda x: x/10):
    for i in g:
       # do what you need to do with individual items
    # now do what you need to do with the whole group

Mon intérêt particulier à cet égard était la nécessité de recourir à un générateur pour soumettre des modifications allant jusqu'à 1 000 lots à l'API gmail:

    messages = a_generator_which_would_not_be_smart_as_a_list
    for idx, batch in groupby(messages, lambda x: x/1000):
        batch_request = BatchHttpRequest()
        for message in batch:
            batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
        http = httplib2.Http()
        self.credentials.authorize(http)
        batch_request.execute(http=http)

À propos de la solution donnée par J.F. Sebastian ici :

def chunker(iterable, chunksize):
    return zip(*[iter(iterable)]*chunksize)

C'est intelligent, mais a un inconvénient: retournez toujours le tuple. Comment obtenir de la ficelle à la place?
Bien sûr, vous pouvez écrire ''. Join (chunker (...)) , mais le tuple temporaire est quand même construit.

Vous pouvez vous débarrasser du tuple temporaire en écrivant votre propre zip , comme suit:

class IteratorExhausted(Exception):
    pass

def translate_StopIteration(iterable, to=IteratorExhausted):
    for i in iterable:
        yield i
    raise to # StopIteration would get ignored because this is generator,
             # but custom exception can leave the generator.

def custom_zip(*iterables, reductor=tuple):
    iterators = tuple(map(translate_StopIteration, iterables))
    while True:
        try:
            yield reductor(next(i) for i in iterators)
        except IteratorExhausted: # when any of iterators get exhausted.
            break

Alors

def chunker(data, size, reductor=tuple):
    return custom_zip(*[iter(data)]*size, reductor=reductor)

Exemple d'utilisation:

>>> for i in chunker('12345', 2):
...     print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
...     print(repr(i))
...
'12'
'34'

J'aime cette approche. Il se sent simple et pas magique, supporte tous les types itérables et ne nécessite pas d’importation.

def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
    chunk = tuple(next(it) for _ in range(chunk_size))
    if not chunk:
        break
    yield chunk

Je ne veux jamais que mes morceaux soient rembourrés, cette exigence est donc essentielle. Je trouve que la capacité de travailler sur n'importe quel itérable est également une exigence. Compte tenu de cela, j'ai décidé d'étendre la réponse acceptée, https://stackoverflow.com/a/434411/1074659 .

Les performances pèsent légèrement sur cette approche si le remplissage n’est pas souhaité, en raison de la nécessité de comparer et de filtrer les valeurs remplies. Toutefois, pour les gros morceaux, cet utilitaire est très performant.

#!/usr/bin/env python3
from itertools import zip_longest


_UNDEFINED = object()


def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
    """
    Collect data into chunks and optionally pad it.

    Performance worsens as `chunksize` approaches 1.

    Inspired by:
        https://docs.python.org/3/library/itertools.html#itertools-recipes

    """
    args = [iter(iterable)] * chunksize
    chunks = zip_longest(*args, fillvalue=fillvalue)
    yield from (
        filter(lambda val: val is not _UNDEFINED, chunk)
        if chunk[-1] is _UNDEFINED
        else chunk
        for chunk in chunks
    ) if fillvalue is _UNDEFINED else chunks
def chunker(iterable, n):
    """Yield iterable in chunk sizes.

    >>> chunks = chunker('ABCDEF', n=4)
    >>> chunks.next()
    ['A', 'B', 'C', 'D']
    >>> chunks.next()
    ['E', 'F']
    """
    it = iter(iterable)
    while True:
        chunk = []
        for i in range(n):
            try:
                chunk.append(next(it))
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()

Voici un bloc sans importations qui prend en charge les générateurs:

def chunks(seq, size):
    it = iter(seq)
    while True:
        ret = tuple(next(it) for _ in range(size))
        if len(ret) == size:
            yield ret
        else:
            raise StopIteration()

Exemple d'utilisation:

>>> def foo():
...     i = 0
...     while True:
...         i += 1
...         yield i
...
>>> c = chunks(foo(), 3)
>>> c.next()
(1, 2, 3)
>>> c.next()
(4, 5, 6)
>>> list(chunks('abcdefg', 2))
[('a', 'b'), ('c', 'd'), ('e', 'f')]

Cela ne semble pas être une belle façon de faire cela. Ici est une page comportant un certain nombre de méthodes, notamment:

def split_seq(seq, size):
    newseq = []
    splitsize = 1.0/size*len(seq)
    for i in range(size):
        newseq.append(seq[int(round(i*splitsize)):int(round((i+1)*splitsize))])
    return newseq

Si les listes ont la même taille, vous pouvez les combiner en listes de 4-tuples avec zip () . Par exemple:

# Four lists of four elements each.

l1 = range(0, 4)
l2 = range(4, 8)
l3 = range(8, 12)
l4 = range(12, 16)

for i1, i2, i3, i4 in zip(l1, l2, l3, l4):
    ...

Voici ce que la fonction zip () produit:

>>> print l1
[0, 1, 2, 3]
>>> print l2
[4, 5, 6, 7]
>>> print l3
[8, 9, 10, 11]
>>> print l4
[12, 13, 14, 15]
>>> print zip(l1, l2, l3, l4)
[(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]

Si les listes sont volumineuses et que vous ne souhaitez pas les combiner en une liste plus grande, utilisez itertools.izip () , qui produit un itérateur, plutôt qu'une liste.

from itertools import izip

for i1, i2, i3, i4 in izip(l1, l2, l3, l4):
    ...

Solution unique, ad hoc, permettant de parcourir une liste x en morceaux de taille 4 -

for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
    ... do something with a, b, c and d ...

Au début, je l'avais conçu pour scinder les chaînes en sous-chaînes afin d'analyser les chaînes contenant des caractères hexadécimaux.
Aujourd'hui, je l'ai transformé en générateur complexe, mais toujours simple.

def chunker(iterable, size, reductor, condition):
    it = iter(iterable)
    def chunk_generator():
        return (next(it) for _ in range(size))
    chunk = reductor(chunk_generator())
    while condition(chunk):
        yield chunk
        chunk = reductor(chunk_generator())

Arguments:

Les plus évidents

  • iterable désigne tout itératif / itérateur / générateur contenant / générant / itérant sur des données d'entrée,
  • taille est bien sûr la taille du bloc que vous voulez obtenir,

Plus intéressant

  • réducteur est un appelable, qui reçoit le générateur itérant sur le contenu du bloc.
    Je m'attendrais à ce qu'il retourne une séquence ou une chaîne, mais je ne l'exige pas.

    Vous pouvez transmettre cet argument, par exemple, liste , tuple , set , frozenset ,
    ou quelque chose d'amateur. Je passerais cette fonction en retournant la chaîne
    (à condition que iterable contienne / génère / itère sur des chaînes):

    def concatenate(iterable):
        return ''.join(iterable)
    

    Notez que réducteur peut provoquer la fermeture du générateur en soulevant une exception.

  • condition est un appelable qui reçoit tout ce que réducteur a renvoyé.
    Il décide d’approuver & amp; le céder (en retournant tout ce qui donne l'évaluation sur True ),
    ou pour le refuser & amp; terminer le travail du générateur (en renvoyant autre chose ou en soulevant une exception).

    Lorsque le nombre d'éléments dans iterable n'est pas divisible par taille , lorsque il est épuisé, réducteur recevoir un générateur générant moins d’éléments que taille .
    Appelons ces éléments derniers éléments .

    J'ai invité deux fonctions à passer comme argument:

    • lambda x: x - les éléments de dernière durée seront cédés.

    • lambda x: len (x) == < size > - les éléments de dernière seront rejetés.
      remplacer < taille > en utilisant un nombre égal à taille

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top