Was ist die "pythonischste" Methode, um eine Liste in Blöcken zu durchlaufen?


488

Ich habe ein Python-Skript, das als Eingabe eine Liste von Ganzzahlen verwendet, die ich mit jeweils vier Ganzzahlen bearbeiten muss. Leider habe ich keine Kontrolle über die Eingabe, oder ich hätte sie als Liste von Tupeln mit vier Elementen übergeben. Derzeit iteriere ich folgendermaßen darüber:

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]

Es sieht jedoch sehr nach "C-think" aus, was mich vermuten lässt, dass es eine pythonischere Art gibt, mit dieser Situation umzugehen. Die Liste wird nach dem Iterieren verworfen, sodass sie nicht beibehalten werden muss. Vielleicht wäre so etwas besser?

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

Trotzdem fühlt es sich nicht ganz richtig an. : - /

Verwandte Frage: Wie teilt man eine Liste in Python in gleich große Teile auf?


3
Ihr Code funktioniert nicht, wenn die Listengröße kein Vielfaches von vier ist.
Pedro Henriques

5
Ich erweitere () die Liste so, dass ihre Länge ein Vielfaches von vier ist, bevor sie so weit kommt.
Ben Blank

4
@ ΤΖΩΤΖΙΟΥ - Die Fragen sind sehr ähnlich, aber nicht ganz doppelt. Es ist "aufgeteilt in eine beliebige Anzahl von Blöcken der Größe N" vs. "aufgeteilt in N Blöcke einer beliebigen Größe". :-)
Ben Blank


Antworten:


340

Geändert von den Rezepten Abschnitt von Pythons itertools docs:

from itertools import zip_longest

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

Beispiel
Im Pseudocode, um das Beispiel knapp zu halten.

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

Hinweis: Verwenden Sie unter Python 2 izip_longestanstelle von zip_longest.


67
Endlich die Gelegenheit, in einer Python-Session damit herumzuspielen. Für diejenigen, die genauso verwirrt sind wie ich, führt dies den gleichen Iterator mehrmals an izip_longest weiter, wodurch aufeinanderfolgende Werte derselben Sequenz anstelle von gestreiften Werten aus separaten Sequenzen verwendet werden. Ich liebe es!
Ben Blank

6
Was ist der beste Weg, um den Füllwert wieder herauszufiltern? ([Element für Element in Elementen, wenn Element kein Füllwert ist] für Elemente im Zackenbarsch (iterierbar))?
Gotgenes

14
Ich vermute, dass die Leistung dieses Grouper-Rezepts für Chunks mit einer Größe von 256 KB sehr schlecht sein wird, da izip_longest256.000 Argumente eingespeist werden.
Anatoly Techtonik

13
An mehreren Stellen sagen Kommentatoren: "Als ich endlich herausgefunden habe, wie das funktioniert ..." Vielleicht ist eine Erklärung erforderlich. Insbesondere die Liste der Iteratoren Aspekt.
LondonRob

6
Gibt es eine Möglichkeit, dies zu verwenden, ohne Noneden letzten Block aufzufüllen?
CMCDragonkai

420
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)

Einfach. Einfach. Schnell. Funktioniert mit jeder Sequenz:

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']

16
Die Version von @Carlos Crasborn funktioniert für alle iterierbaren Elemente (nicht nur für Sequenzen wie den obigen Code). es ist prägnant und wahrscheinlich genauso schnell oder sogar schneller. Obwohl es für Leute, die mit dem itertoolsModul nicht vertraut sind, etwas dunkel (unklar) sein könnte .
JFS

1
Einverstanden. Dies ist der allgemeinste und pythonischste Weg. Klar und prägnant. (und arbeitet an App Engine)
Matt Williamson

3
Beachten Sie, dass chunkera zurückgegeben wird generator. Ersetzen Sie die Rückkehr zu: return [...]um eine Liste zu erhalten.
Dror

11
Anstatt einen Funktionsaufbau zu schreiben und dann einen Generator zurückzugeben, können Sie auch einen Generator direkt schreiben, indem Sie yield: for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]. Ich bin mir nicht sicher, ob dies intern in einem relevanten Aspekt anders gehandhabt wird, aber es könnte sogar ein bisschen klarer sein.
Alfe

3
Beachten Sie, dass dies nur für Sequenzen funktioniert, die den Zugriff von Elementen über den Index unterstützen, und nicht für generische Iteratoren, da sie möglicherweise keine __getitem__Methode unterstützen.
Apollo

135

Ich bin ein Fan von

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size

Wie verhält es sich, wenn len (ints) kein Vielfaches der chunkSize ist?
PlsWork

3
@AnnaVopureta enthält chunk1, 2 oder 3 Elemente für den letzten Elementstapel. In dieser Frage erfahren Sie, warum Slice-Indizes außerhalb der Grenzen liegen können .
Boris

22
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)

Ein anderer Weg:

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

2
+1 für die Verwendung von Generatoren, Nähte wie die "pythonischste" aller vorgeschlagenen Lösungen
Sergey Golovchenko

7
Es ist ziemlich lang und ungeschickt für etwas so Einfaches, das überhaupt nicht sehr pythonisch ist. Ich bevorzuge S. Lotts Version
Zenazn

4
@zenazn: Dies wird auf Generatorinstanzen funktionieren, Slicing nicht
Janus Troelsen

Zusätzlich zur ordnungsgemäßen Arbeit mit Generatoren und anderen nicht in Scheiben geschnittenen Iteratoren erfordert die erste Lösung auch keinen "Füllwert", wenn der endgültige Block kleiner als ist size, was manchmal wünschenswert ist.
Dano

1
Auch +1 für Generatoren. Andere Lösungen erfordern einen lenAnruf und funktionieren daher nicht mit anderen Generatoren.
Cuadue


11

Die ideale Lösung für dieses Problem funktioniert mit Iteratoren (nicht nur Sequenzen). Es sollte auch schnell sein.

Dies ist die Lösung, die in der Dokumentation für itertools bereitgestellt wird:

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

Wenn ich Ipythons %timeitauf meinem Mac Book Air verwende, erhalte ich 47,5 US-Dollar pro Schleife.

Dies funktioniert jedoch wirklich nicht für mich, da die Ergebnisse so gepolstert sind, dass sie gleich große Gruppen sind. Eine Lösung ohne Polsterung ist etwas komplizierter. Die naivste Lösung könnte sein:

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

Einfach, aber ziemlich langsam: 693 us pro Schleife

Die beste Lösung, die ich islicefür die innere Schleife finden konnte:

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

Mit dem gleichen Datensatz erhalte ich 305 us pro Schleife.

Da es nicht möglich ist, eine reine Lösung schneller zu erhalten, biete ich der folgenden Lösung eine wichtige Einschränkung: Wenn Ihre Eingabedaten Instanzen enthalten filldata, können Sie eine falsche Antwort erhalten.

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

Diese Antwort gefällt mir wirklich nicht, aber sie ist deutlich schneller. 124 us pro Schleife


Sie können die Laufzeit für Rezept Nr. 3 um ~ 10-15% reduzieren, indem Sie es in die C-Ebene verschieben (ohne itertoolsImporte; mapmuss Py3 sein mapoder imap) : def grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n))))). Ihre letzte Funktion kann durch Verwendung eines Sentinels weniger spröde gemacht werden: Entfernen Sie das fillvalueArgument; Fügen Sie eine erste Zeile hinzu fillvalue = object()und ändern Sie dann das ifHäkchen in if i[-1] is fillvalue:und die Zeile, in die es steuert yield tuple(v for v in i if v is not fillvalue). Garantiert, dass kein Wert in iterablemit dem Füllwert verwechselt werden kann.
ShadowRanger

Übrigens, Daumen hoch auf # 4. Ich wollte meine Optimierung von # 3 als bessere Antwort (in Bezug auf die Leistung) veröffentlichen als bisher, aber mit der Optimierung, um sie zuverlässig zu machen, läuft die belastbare # 4 doppelt so schnell wie die optimierte # 3; Ich hatte nicht erwartet, dass eine Lösung mit Python-Level-Loops (und keinen theoretischen algorithmischen Unterschieden AFAICT) gewinnen würde. Ich gehe davon aus, dass # 3 aufgrund der Kosten für das Erstellen / Iterieren von isliceObjekten verliert (# 3 gewinnt, wenn nes relativ groß ist, z. B. die Anzahl der Gruppen ist klein, aber das optimiert für einen ungewöhnlichen Fall), aber ich habe nicht erwartet, dass es so ist extrem.
ShadowRanger

Für # 4 wird der erste Zweig der Bedingung immer nur bei der letzten Iteration (dem letzten Tupel) genommen. Anstatt das endgültige Tupel erneut zu rekonstituieren, speichern Sie das Modulo der Länge des Originals oben iterierbar und entfernen Sie damit das unerwünschte Auffüllen izip_longestdes endgültigen Tupels : yield i[:modulo]. argsTupeln Sie die Variable anstelle einer Liste : args = (iter(iterable),) * n. Rasiert noch ein paar Taktzyklen aus. Wenn wir den Füllwert ignorieren und annehmen None, kann die Bedingung if None in ifür noch mehr Taktzyklen gelten.
Kumba

1
@Kumba: Ihr erster Vorschlag geht davon aus, dass die Eingabe eine bekannte Länge hat. Wenn es sich um einen Iterator / Generator handelt, nicht um eine Sammlung mit bekannter Länge, muss nichts zwischengespeichert werden. Es gibt sowieso keinen wirklichen Grund, eine solche Optimierung zu verwenden. Sie optimieren den ungewöhnlichen Fall (den letzten yield), während der häufige Fall nicht betroffen ist.
ShadowRanger

10

Ich brauchte eine Lösung, die auch mit Sets und Generatoren funktioniert. Ich konnte mir nichts sehr kurzes und hübsches einfallen lassen, aber es ist zumindest gut lesbar.

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]]

Einstellen:

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

Generator:

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

8

Ähnlich wie bei anderen Vorschlägen, aber nicht genau identisch, mache ich das gerne so, weil es einfach und leicht zu lesen ist:

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)

Auf diese Weise erhalten Sie nicht den letzten Teilblock. Wenn Sie (9, None, None, None)als letzten Block erhalten möchten , verwenden Sie einfach izip_longestvon itertools.


kann verbessert werden mitzip(*([it]*4))
Jean-François Fabre

@ Jean-François Fabre: Aus Sicht der Lesbarkeit sehe ich es nicht als Verbesserung. Und es ist auch etwas langsamer. Es ist eine Verbesserung, wenn Sie Golf spielen, was ich nicht bin.
kriss

Nein, ich spiele nicht Golf, aber was ist, wenn Sie 10 Argumente haben? Ich habe dieses Konstrukt auf einer offiziellen Seite gelesen. Aber natürlich kann ich es momentan nicht finden :)
Jean-François Fabre

@ Jean-François Fabre: Wenn ich 10 Argumente oder eine variable Anzahl von Argumenten habe, ist dies eine Option, aber ich würde lieber schreiben: zip (* (it,) * 10)
kriss

Recht! das habe ich gelesen nicht die Liste Sachen, die ich erfunden habe :)
Jean-François Fabre

8

Wenn es Ihnen nichts ausmacht, ein externes Paket zu verwenden, können Sie es iteration_utilities.grouperab 1 verwenden . Es unterstützt alle iterablen Elemente (nicht nur Sequenzen):iteration_utilties

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

welche druckt:

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

Falls die Länge nicht ein Vielfaches der Gruppengröße ist, unterstützt sie auch das Füllen (die unvollständige letzte Gruppe) oder das Abschneiden (das Verwerfen der unvollständigen letzten Gruppe) der letzten:

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)

Benchmarks

Ich habe mich auch entschlossen, die Laufzeit einiger der genannten Ansätze zu vergleichen. Es handelt sich um ein Protokoll-Protokoll-Diagramm, das anhand einer Liste unterschiedlicher Größe in Gruppen von "10" Elementen gruppiert wird. Für qualitative Ergebnisse: Niedriger bedeutet schneller:

Geben Sie hier die Bildbeschreibung ein

Zumindest in diesem Benchmark iteration_utilities.grouperschneidet der am besten ab. Gefolgt von der Annäherung von Craz .

Der Benchmark wurde mit 1 erstellt . Der Code, der zum Ausführen dieses Benchmarks verwendet wurde, war:simple_benchmark

import iteration_utilities
import itertools
from itertools import zip_longest

def consume_all(it):
    return iteration_utilities.consume(it, None)

import simple_benchmark
b = simple_benchmark.BenchmarkBuilder()

@b.add_function()
def grouper(l, n):
    return consume_all(iteration_utilities.grouper(l, n))

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

@b.add_function()
def Craz(iterable, n, fillvalue=None):
    return consume_all(Craz_inner(iterable, n, fillvalue))

def nosklo_inner(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))

@b.add_function()
def nosklo(seq, size):
    return consume_all(nosklo_inner(seq, size))

def SLott_inner(ints, chunk_size):
    for i in range(0, len(ints), chunk_size):
        yield ints[i:i+chunk_size]

@b.add_function()
def SLott(ints, chunk_size):
    return consume_all(SLott_inner(ints, chunk_size))

def MarkusJarderot1_inner(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot1(iterable,size):
    return consume_all(MarkusJarderot1_inner(iterable,size))

def MarkusJarderot2_inner(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))

@b.add_function()
def MarkusJarderot2(iterable,size):
    return consume_all(MarkusJarderot2_inner(iterable,size))

@b.add_arguments()
def argument_provider():
    for exp in range(2, 20):
        size = 2**exp
        yield size, simple_benchmark.MultiArgument([[0] * size, 10])

r = b.run()

1 Haftungsausschluss: Ich bin der Autor der Bibliotheken iteration_utilitiesund simple_benchmark.


7

Da es noch niemand erwähnt hat, ist hier eine zip()Lösung:

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

Dies funktioniert nur, wenn die Länge Ihrer Sequenz immer durch die Blockgröße teilbar ist oder Sie sich nicht für einen nachfolgenden Block interessieren, wenn dies nicht der Fall ist.

Beispiel:

>>> 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')]

Oder verwenden Sie itertools.izip , um einen Iterator anstelle einer Liste zurückzugeben:

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

Die Polsterung kann mit der Antwort von @ ΤΖΩΤΖΙΟΥ behoben werden :

>>> 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)

5

Die Verwendung von map () anstelle von zip () behebt das Auffüllproblem in JF Sebastians Antwort:

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

Beispiel:

>>> 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')]

2
Dies wird besser mit itertools.izip_longest(Py2) / itertools.zip_longest(Py3) gehandhabt; Diese Verwendung von mapist doppelt veraltet und in Py3 nicht verfügbar (Sie können sie nicht Noneals Mapper-Funktion übergeben. Sie wird beendet, wenn die kürzeste iterierbare Funktion erschöpft ist, nicht die längste; sie wird nicht aufgefüllt).
ShadowRanger

4

Ein anderer Ansatz wäre die Verwendung der Zwei-Argumente-Form von iter:

from itertools import islice

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

Dies kann leicht angepasst werden, um Polsterung zu verwenden (dies ähnelt der Antwort von 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)

Diese können sogar für eine optionale Polsterung kombiniert werden:

_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)

1
vorzuziehen, da Sie die Möglichkeit haben, die Polsterung wegzulassen!
n611x007

3

Wenn die Liste groß ist, ist die Verwendung eines Generators die leistungsstärkste Methode, um dies zu tun:

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,)

(Ich denke, dass MizardXs itertools-Vorschlag funktional dem entspricht.)
Robert Rossney

1
(Eigentlich, nein, ich nicht. Itertools.islice gibt einen Iterator zurück, aber er verwendet keinen vorhandenen.)
Robert Rossney

Es ist schön und einfach, aber aus irgendeinem Grund auch ohne Konvertierung in Tupel 4-7 mal langsamer als die akzeptierte Grouper-Methode auf iterable = range(100000000)& chunksizebis zu 10000.
Valentas

Im Allgemeinen würde ich diese Methode jedoch empfehlen, da die akzeptierte Methode extrem langsam sein kann, wenn nach dem letzten Element gesucht
Valentas

3

Die Verwendung kleiner Funktionen und Dinge gefällt mir wirklich nicht. Ich bevorzuge es, nur Scheiben zu verwenden:

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:
    ...

schön, aber nicht gut für einen unbestimmten Stream, der nicht bekannt ist len. Sie können einen Test mit itertools.repeatoder durchführen itertools.cycle.
n611x007

1
Außerdem wird Speicherplatz [...for...] (...for...)
verbraucht,

2

So vermeiden Sie alle Konvertierungen in eine Liste import itertoolsund:

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

Produziert:

... 
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]
>>> 

Ich habe es überprüft groupbyund es wird nicht in eine Liste konvertiert oder verwendetlen Ich denke, dies verzögert die Auflösung jedes Werts, bis er tatsächlich verwendet wird. Leider schien keine der verfügbaren Antworten (zu diesem Zeitpunkt) diese Variation zu bieten.

Wenn Sie nacheinander mit jedem Element umgehen müssen, verschachteln Sie natürlich eine for-Schleife über 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

Mein besonderes Interesse daran war die Notwendigkeit, einen Generator zu verwenden, um Änderungen in Stapeln von bis zu 1000 an die Google Mail-API zu senden:

    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)

Was ist, wenn die Liste, die Sie aufteilen, etwas anderes als eine Folge aufsteigender Ganzzahlen ist?
PaulMcG

@PaulMcGuire siehe groupby ; Wenn eine Funktion zur Beschreibung der Reihenfolge gegeben ist, können Elemente des iterablen Elements alles sein, oder?
John Mee

1
Ja, ich kenne mich mit groupby aus. Wenn Nachrichten jedoch die Buchstaben "ABCDEFG" groupby(messages, lambda x: x/3)wären, würden Sie einen TypeError (für den Versuch, eine Zeichenfolge durch ein int zu teilen) und keine Gruppierungen mit drei Buchstaben erhalten. Wenn du es getan groupby(enumerate(messages), lambda x: x[0]/3)hättest, hättest du vielleicht etwas. Aber das hast du in deinem Beitrag nicht gesagt.
PaulMcG

2

Mit NumPy ist es einfach:

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

Ausgabe:

1 2
3 4
5 6
7 8

2
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()

2

Sofern ich nichts vermisse, wurde die folgende einfache Lösung mit Generatorausdrücken nicht erwähnt. Es wird davon ausgegangen, dass sowohl die Größe als auch die Anzahl der Chunks bekannt sind (was häufig der Fall ist) und dass keine Polsterung erforderlich ist:

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))

1

Bei Ihrer zweiten Methode würde ich auf diese Weise zur nächsten 4er-Gruppe übergehen:

ints = ints[4:]

Ich habe jedoch keine Leistungsmessung durchgeführt, daher weiß ich nicht, welche effizienter sein könnte.

Trotzdem würde ich normalerweise die erste Methode wählen. Es ist nicht schön, aber das ist oft eine Folge der Schnittstelle zur Außenwelt.


1

Noch eine Antwort, deren Vorteile sind:

1) Leicht verständlich
2) Funktioniert mit allen iterierbaren Sequenzen, nicht nur mit Sequenzen (einige der oben genannten Antworten ersticken an Dateihandles)
3) Lädt den Block nicht auf einmal in den Speicher
4) Erstellt keine lange Liste von Verweisen auf der gleiche Iterator im Speicher
5) Kein Auffüllen der Füllwerte am Ende der Liste

Davon abgesehen habe ich es nicht zeitlich festgelegt, sodass es möglicherweise langsamer ist als einige der clevereren Methoden, und einige der Vorteile sind angesichts des Anwendungsfalls möglicherweise irrelevant.

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 

Update:
Einige Nachteile aufgrund der Tatsache, dass die innere und die äußere Schleife Werte vom selben Iterator abrufen:
1) Fortfahren funktioniert in der äußeren Schleife nicht wie erwartet - es wird nur mit dem nächsten Element fortgefahren, anstatt einen Block zu überspringen . Dies scheint jedoch kein Problem zu sein, da in der äußeren Schleife nichts zu testen ist.
2) break funktioniert im inneren Regelkreis nicht wie erwartet - die Steuerung wird mit dem nächsten Element im Iterator wieder im inneren Regelkreis beendet. Um ganze Blöcke zu überspringen, wickeln Sie entweder den inneren Iterator (ii oben) in ein Tupel, z. B. for c in tuple(ii), oder setzen Sie ein Flag und erschöpfen Sie den Iterator.


1
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

+1 ohne Polsterung; Ihre und bcoughlan ‚s ist sehr ähnlich
n611x007

1

Sie können die Partitions- oder Chunks- Funktion aus der Funcy- Bibliothek verwenden:

from funcy import partition

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

Diese Funktionen haben auch Iteratorversionen ipartitionundichunks , die in diesem Fall effizienter sind.

Sie können auch einen Blick auf deren Implementierung werfen .


1

Über die J.F. Sebastian hier gegebene Lösung :

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

Es ist klug, hat aber einen Nachteil - immer Tupel zurückgeben. Wie bekomme ich stattdessen einen String?
Natürlich kannst du schreiben''.join(chunker(...)) , aber das temporäre Tupel ist trotzdem aufgebaut.

Sie können das temporäre Tupel loswerden, indem Sie Folgendes schreiben zip:

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

Dann

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

Anwendungsbeispiel:

>>> 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'

2
Keine Kritik für Sie, um Ihre Antwort zu ändern, sondern ein Kommentar: Code ist eine Haftung. Je mehr Code Sie schreiben, desto mehr Platz schaffen Sie, damit Fehler ausgeblendet werden können. Unter diesem Gesichtspunkt zipscheint es nicht die beste Idee zu sein, das vorhandene neu zu schreiben, anstatt es zu verwenden.
Alfe

1

Ich mag diesen Ansatz. Es fühlt sich einfach und nicht magisch an und unterstützt alle iterierbaren Typen und erfordert keine Importe.

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

1

Ich möchte nie, dass meine Brocken gepolstert werden, daher ist diese Anforderung unerlässlich. Ich finde, dass die Fähigkeit, an jedem Iterable zu arbeiten, ebenfalls Voraussetzung ist. Angesichts dessen habe ich mich entschlossen, die akzeptierte Antwort https://stackoverflow.com/a/434411/1074659 zu erweitern .

Die Leistung nimmt bei diesem Ansatz leicht ab, wenn keine Auffüllung gewünscht wird, da die aufgefüllten Werte verglichen und gefiltert werden müssen. Für große Blockgrößen ist dieses Dienstprogramm jedoch sehr leistungsfähig.

#!/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

1

Hier ist ein Chunker ohne Importe, der Generatoren unterstützt:

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

Anwendungsbeispiel:

>>> 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')]

1

Mit Python 3.8 können Sie den Walross-Operator und verwenden itertools.islice.

from itertools import islice

list_ = [i for i in range(10, 100)]

def chunker(it, size):
    iterator = iter(it)
    while chunk := list(islice(iterator, size)):
        print(chunk)
In [2]: chunker(list_, 10)                                                         
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69]
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

0

Es scheint keinen schönen Weg zu geben, dies zu tun. Hier ist eine Seite mit einer Reihe von Methoden, darunter:

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

0

Wenn die Listen dieselbe Größe haben, können Sie sie zu Listen mit 4 Tupeln mit kombinieren zip(). Zum Beispiel:

# 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):
    ...

Folgendes zip()erzeugt die Funktion:

>>> 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)]

Wenn die Listen groß sind und Sie sie nicht zu einer größeren Liste kombinieren möchten, verwenden Sie itertools.izip(), wodurch anstelle einer Liste ein Iterator erstellt wird.

from itertools import izip

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

0

Einzeilige Ad-hoc-Lösung zum Durchlaufen einer Liste xin großen Stücken 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 ...
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.