wie man eine iterable in Blöcke konstanter Größe aufteilt


86

Mögliches Duplizieren:
Wie teilt man eine Liste in Python in gleich große Teile auf?

Ich bin überrascht, dass ich keine "Batch" -Funktion finden konnte, die als Eingabe eine iterable und eine iterable von iterables zurückgibt.

Zum Beispiel:

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

oder:

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

Jetzt schrieb ich, was ich für einen ziemlich einfachen Generator hielt:

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

Aber das Obige gibt mir nicht das, was ich erwartet hätte:

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]

Ich habe also etwas verpasst und dies zeigt wahrscheinlich mein völliges Unverständnis für Python-Generatoren. Möchte mich jemand in die richtige Richtung weisen?

[Bearbeiten: Ich habe schließlich festgestellt, dass das obige Verhalten nur auftritt, wenn ich dies in ipython und nicht in python selbst ausführe]


Gute Frage, gut geschrieben, aber sie existiert bereits und wird Ihr Problem lösen.
Josh Smeaton

7
IMO ist dies nicht wirklich ein Duplikat. Die andere Frage konzentriert sich auf Listen anstelle von Iteratoren, und die meisten dieser Antworten erfordern len (), was für Iteratoren unerwünscht ist. Aber eh, die aktuell akzeptierte Antwort hier erfordert auch len (), also ...
dequis

7
Dies ist eindeutig kein Duplikat. Die anderen Fragen und Antworten funktionieren nur für Listen , und bei dieser Frage geht es um die Verallgemeinerung auf alle iterablen Elemente. Dies ist genau die Frage, an die ich gedacht hatte, als ich hierher kam.
Mark E. Haase

1
@JoshSmeaton @casperOne Dies ist kein Duplikat und die akzeptierte Antwort ist nicht korrekt. Die verknüpfte doppelte Frage dient zur Liste und ist iterierbar. Liste bietet len ​​() Methode, aber iterable bietet keine len () Methode und die Antwort wäre anders ohne len () Dies ist die richtige Antwort: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath

@TrideepRath yep, ich habe für die Wiedereröffnung gestimmt.
Josh Smeaton

Antworten:


124

Dies ist wahrscheinlich effizienter (schneller)

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

Beispiel mit Liste

data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # list of data 

for x in batch(data, 3):
    print(x)

# Output

[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9, 10]

Es wird vermieden, neue Listen zu erstellen.


4
Für die Aufzeichnung ist dies die schnellste Lösung, die ich gefunden habe: meine = 4,5 s, deine = 0,43 s, Donkopotamus = 14,8 s
Mathieu

75
Ihr Stapel akzeptiert tatsächlich eine Liste (mit len ​​()), nicht iterierbar (ohne len ())
tdihp

30
Dies ist schneller, da es keine Lösung für das Problem ist. Das Grouper-Rezept von Raymond Hettinger - derzeit darunter - ist das, wonach Sie nach einer allgemeinen Lösung suchen, bei der das Eingabeobjekt keine len- Methode haben muss.
Robert E Mealey

7
Warum benutzt du min ()? Ohne min()Code ist das völlig richtig!
Pavel Patrin

21
Iterables haben nicht len(), Sequenzen habenlen()
Kos

61

FWIW, die Rezepte im Modul itertools bieten dieses Beispiel:

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

Es funktioniert so:

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

13
Dies ist nicht genau das, was ich brauchte, da es das letzte Element mit einem Satz von None auffüllt. Das heißt, Keine ist ein gültiger Wert in den Daten, die ich tatsächlich mit meiner Funktion verwende. Stattdessen benötige ich etwas, das den letzten Eintrag nicht auffüllt.
Mathieu

12
@mathieu Ersetzen izip_longestdurch izip, wodurch nicht die letzten Einträge aufgefüllt werden, sondern Einträge abgeschnitten werden, wenn einige der Elemente ausgehen.
GoogieK

3
Sollte zip_longest / zip in Python 3 sein
Peter Gerdes

5
@GoogieK füllt in for x, y in enumerate(grouper(3, xrange(10))): print(x,y)der Tat keine Werte, sondern löscht nur das unvollständige Segment insgesamt.
Kadrach

3
Als Einzeiler, der das letzte Element fallen lässt, wenn es unvollständig ist : list(zip(*[iter(iterable)] * n)). Dies muss das sauberste Stück Python-Code sein, das ich je gesehen habe.
Le Frite

31

Wie andere angemerkt haben, macht der von Ihnen angegebene Code genau das, was Sie wollen. Für einen anderen Ansatz mit itertools.islicekönnte man eine sehen Beispiel für das folgende Rezept sehen:

from itertools import islice, chain

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

1
@abhilash Nein ... dieser Code verwendet den Aufruf, um next()zu bewirken, dass ein StopIterationeinmal sourceitererschöpft ist, wodurch der Iterator beendet wird. Ohne den Aufruf nextwürde es weiterhin unbegrenzt leere Iteratoren zurückgeben.
Donkopotamus

7
Ich hatte zu ersetzen batchiter.next()mit next(batchiter)zu der obigen Code Arbeit in Python zu machen 3.
Martin Wiebusch

2
Hinweis auf einen Kommentar aus dem verlinkten Artikel: "Sie sollten eine Warnung hinzufügen, dass eine Charge vollständig verbraucht sein muss, bevor Sie mit der nächsten fortfahren können." Die Ausgabe davon sollte mit etwas wie verbraucht werden : map(list, batch(xrange(10), 3)). Tun: list(batch(xrange(10), 3)führt zu unerwarteten Ergebnissen.
Nathan Buesgens

2
Funktioniert nicht auf py3. .next()muss geändert werden next(..), und list(batch(range(0,10),3))wirftRuntimeError: generator raised StopIteration
Mathieu

1
@mathieu: Wickeln Sie die whileSchleife in try:/ except StopIteration: return, um das letztere Problem zu beheben.
ShadowRanger

13

Ich habe nur eine Antwort gegeben. Jetzt bin ich jedoch der Meinung, dass die beste Lösung darin besteht, keine neuen Funktionen zu schreiben. More-itertools enthält viele zusätzliche Tools und chunkedgehört dazu.


Dies ist in der Tat die passendste Antwort (obwohl ein weiteres Paket installiert werden muss), und es gibt auch ichunkediterable Antworten.
viddik13

10

Seltsam, scheint für mich in Python 2.x gut zu funktionieren

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

Tolle Antwort, da es nichts importieren muss und intuitiv zu lesen ist.
Ojunk

8

Dies ist ein sehr kurzes Code-Snippet, das ich kenne und das nicht lenunter Python 2 und 3 (nicht meiner Erstellung) verwendet und funktioniert:

def chunks(iterable, size):
    from itertools import chain, islice
    iterator = iter(iterable)
    for first in iterator:
        yield list(chain([first], islice(iterator, size - 1)))

6

Lösung für Python 3.8, wenn Sie mit Iterables arbeiten, die keine lenFunktion definieren , und erschöpft sind:

def batcher(iterable, batch_size):
    while batch := list(islice(iterable, batch_size)):
        yield batch

Anwendungsbeispiel:

def my_gen():
    yield from range(10)
 
for batch in batcher(my_gen(), 3):
    print(batch)

>>> [0, 1, 2]
>>> [3, 4, 5]
>>> [6, 7, 8]
>>> [9]

Könnte natürlich auch ohne den Walross-Operator implementiert werden.


1
batcherAkzeptiert in der aktuellen Version einen Iterator, keinen iterierbaren. Dies würde beispielsweise zu einer Endlosschleife mit einer Liste führen. iterator = iter(iterable)Vor dem Starten der whileSchleife sollte wahrscheinlich eine Zeile vorhanden sein .
Daniel Perez

2

Das verwende ich in meinem Projekt. Es behandelt Iterables oder Listen so effizient wie möglich.

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

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

Die bisher beste Antwort funktioniert mit jeder Datenstruktur
Clément Prévost

1

Dies würde für jedes iterable funktionieren.

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

Es würde so funktionieren:

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

PS: Es würde nicht funktionieren, wenn iterable None-Werte hat.


1

Hier ist ein Ansatz mit reduceFunktion.

Einzeiler:

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

Oder besser lesbare Version:

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

Prüfung:

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

1

Eine funktionsfähige Version ohne neue Funktionen in Python 3.8, angepasst an die Antwort von @Atra Azami.

import itertools    

def batch_generator(iterable, batch_size=1):
    iterable = iter(iterable)

    while True:
        batch = list(itertools.islice(iterable, batch_size))
        if len(batch) > 0:
            yield batch
        else:
            break

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

Ausgabe:

[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]

0

Sie können iterierbare Elemente einfach nach ihrem Stapelindex gruppieren.

def batch(items: Iterable, batch_size: int) -> Iterable[Iterable]:
    # enumerate items and group them by batch index
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    # extract items from enumeration tuples
    item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

Dies ist häufig der Fall, wenn Sie innere Iterables sammeln möchten. Hier finden Sie eine erweiterte Version.

def batch_advanced(items: Iterable, batch_size: int, batches_mapper: Callable[[Iterable], Any] = None) -> Iterable[Iterable]:
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    if batches_mapper:
        item_batches = (batches_mapper(t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    else:
        item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

Beispiele:

print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, tuple)))
# [(1, 9, 3, 5), (2, 4, 2)]
print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, list)))
# [[1, 9, 3, 5], [2, 4, 2]]

0

Zugehörige Funktionen, die Sie möglicherweise benötigen:

def batch(size, i):
    """ Get the i'th batch of the given size """
    return slice(size* i, size* i + size)

Verwendung:

>>> [1,2,3,4,5,6,7,8,9,10][batch(3, 1)]
>>> [4, 5, 6]

Es erhält den i-ten Stapel aus der Sequenz und kann auch mit anderen Datenstrukturen wie pandas dataframes ( df.iloc[batch(100,0)]) oder numpy array ( array[batch(100,0)]) arbeiten.


0
from itertools import *

class SENTINEL: pass

def batch(iterable, n):
    return (tuple(filterfalse(lambda x: x is SENTINEL, group)) for group in zip_longest(fillvalue=SENTINEL, *[iter(iterable)] * n))

print(list(range(10), 3)))
# outputs: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)]
print(list(batch([None]*10, 3)))
# outputs: [(None, None, None), (None, None, None), (None, None, None), (None,)]

0

ich benutze

def batchify(arr, batch_size):
  num_batches = math.ceil(len(arr) / batch_size)
  return [arr[i*batch_size:(i+1)*batch_size] for i in range(num_batches)]
  

0

Nehmen Sie (höchstens) n Elemente, bis sie aufgebraucht sind.

def chop(n, iterable):
    iterator = iter(iterable)
    while chunk := list(take(n, iterator)):
        yield chunk


def take(n, iterable):
    iterator = iter(iterable)
    for i in range(n):
        try:
            yield next(iterator)
        except StopIteration:
            return
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.