Quel est le moyen le plus «pythonique» de parcourir une liste en morceaux?
-
10-07-2019 - |
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?
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 ??code>, 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 queiterable
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 queréducteur
a renvoyé.
Il décide d’approuver & amp; le céder (en retournant tout ce qui donne l'évaluation surTrue
),
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 partaille
, lorsqueil
est épuisé,réducteur
recevoir un générateur générant moins d’éléments quetaille
.
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
-