jak podzielić iterację na kawałki o stałym rozmiarze

85

Możliwy duplikat:
Jak podzielić listę na równe części w Pythonie?

Dziwię się, że nie mogłem znaleźć funkcji „wsadowej”, która jako dane wejściowe przyjęłaby iterowalną wartość i zwróciłaby iterowalną funkcję iterowalną.

Na przykład:

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

lub:

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

Teraz napisałem coś, co uważałem za całkiem prosty generator:

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

Ale powyższe nie daje mi tego, czego bym się spodziewał:

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]

Tak więc coś przeoczyłem i to prawdopodobnie pokazuje mój całkowity brak zrozumienia generatorów Pythona. Czy ktoś chciałby wskazać mi właściwy kierunek?

[Edycja: w końcu zdałem sobie sprawę, że powyższe zachowanie ma miejsce tylko wtedy, gdy uruchamiam to w ipythonie, a nie w samym Pythonie]

mathieu
źródło
Dobre pytanie, dobrze napisane, ale już istnieje i rozwiąże Twój problem.
Josh Smeaton
7
IMO to nie jest tak naprawdę duplikat. Drugie pytanie koncentruje się na listach zamiast na iteratorach, a większość z tych odpowiedzi wymaga len (), co jest niepożądane dla iteratorów. Ale eh, obecnie akceptowana odpowiedź wymaga również len (), więc ...
dequis
7
To oczywiście nie jest duplikatem. Inne pytania i odpowiedzi działają tylko w przypadku list , a to pytanie dotyczy uogólnienia na wszystkie iterowalne, co jest dokładnie pytaniem, które miałem na myśli, kiedy tu przyjechałem.
Mark E. Haase
1
@JoshSmeaton @casperOne to nie jest duplikat, a zaakceptowana odpowiedź jest nieprawidłowa. Połączone zduplikowane pytanie dotyczy listy, a to jest możliwe do iteracji. lista zawiera metodę len (), ale iterowalna nie zapewnia metody len (), a odpowiedź byłaby inna bez użycia len () Oto poprawna odpowiedź: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath
@TrideepRath tak, głosowałem za ponownym otwarciem.
Josh Smeaton

Odpowiedzi:

119

To jest prawdopodobnie bardziej wydajne (szybsze)

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

Przykład użycia listy

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]

Unika tworzenia nowych list.

Carl F.
źródło
4
Dla przypomnienia, jest to najszybsze rozwiązanie, jakie znalazłem: moje = 4,5s, twoje = 0,43s, Donkopotamus = 14,8s
mathieu
74
Twoja partia faktycznie akceptuje listę (z len ()), nie można iterować (bez len ())
tdihp
28
Jest to szybsze, ponieważ nie jest rozwiązaniem problemu. Przepis na grouper autorstwa Raymonda Hettingera - obecnie poniżej tego - jest tym, czego szukasz dla ogólnego rozwiązania, które nie wymaga, aby obiekt wejściowy miał metodę len .
Robert E Mealey
7
Dlaczego używasz min ()? Bez min()kodu jest całkowicie poprawne!
Pavel Patrin
20
Iterables nie mają len(), sekwencje mająlen()
Kos
60

FWIW, receptury w module itertools stanowią przykład:

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

Działa to tak:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
Raymond Hettinger
źródło
13
Nie jest to dokładnie to, czego potrzebowałem, ponieważ wypełnia ostatni element zestawem Brak. tj. Brak jest prawidłową wartością w danych, których faktycznie używam w mojej funkcji, więc zamiast tego potrzebuję czegoś, co nie wypełnia ostatniego wpisu.
mathieu
12
@mathieu Zamień izip_longestz izip, który nie będzie pad ostatnie wpisy, ale zamiast odciąć wpisów kiedy niektóre elementy zaczynają ucieka.
GoogieK
3
Powinien być zip_longest / zip w pythonie 3
Peter Gerdes
5
@GoogieK for x, y in enumerate(grouper(3, xrange(10))): print(x,y)rzeczywiście nie wypełnia wartości, po prostu całkowicie usuwa niekompletny segment.
kadrach
3
W jednej wkładce, które spada do ostatniego elementu niekompletne: list(zip(*[iter(iterable)] * n)). To musi być najładniejszy fragment kodu Pythona, jaki kiedykolwiek widziałem.
Le Frite
31

Jak zauważyli inni, kod, który podałeś, robi dokładnie to, co chcesz. Aby uzyskać inne podejście, itertools.islicemożesz zobaczyć przykład następującego przepisu:

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)
donkopotamus
źródło
1
@abhilash Nie ... ten kod używa wywołania, aby next()spowodować, że StopIterationraz sourceiterjest wyczerpany, kończąc w ten sposób iterator. Bez wywołania do nextniego nadal będzie zwracać puste iteratory w nieskończoność.
donkopotamus
7
Musiałem wymienić batchiter.next()ze next(batchiter)dokonać powyższych prac kodu w Pythonie 3.
Martin Wiebusch
2
wskazując komentarz z połączonego artykułu: „Należy dodać ostrzeżenie, że partia musi zostać całkowicie zużyta, zanim będzie można przejść do następnej”. Wyjście to powinno być spożywane z czymś takim: map(list, batch(xrange(10), 3)). Wykonanie: list(batch(xrange(10), 3)przyniesie nieoczekiwane rezultaty.
Nathan Buesgens,
2
Nie działa na py3. .next()trzeba zmienić na next(..)i list(batch(range(0,10),3))rzucaRuntimeError: generator raised StopIteration
mathieu
1
@mathieu: Zawiń whilepętlę w try:/, except StopIteration: returnaby naprawić ten drugi problem.
ShadowRanger
13

Podałem tylko jedną odpowiedź. Jednak teraz czuję, że najlepszym rozwiązaniem może być brak pisania nowych funkcji. More-itertools zawiera wiele dodatkowych narzędzi i chunkedjest jednym z nich.

Yongwei Wu
źródło
Jest to rzeczywiście najbardziej trafna odpowiedź (nawet jeśli wymaga instalacji jeszcze jednego pakietu), ale jest też to, ichunkedco daje iterowalne.
viddik13
10

Dziwne, wydaje mi się, że działa dobrze w Pythonie 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]
nieznany z nazwiska
źródło
Świetna odpowiedź, ponieważ nie trzeba niczego importować i jest intuicyjny w czytaniu.
ojunk 23.07.19
8

To jest bardzo krótki fragment kodu, który wiem, że nie używa leni działa zarówno pod Pythonem 2, jak i 3 (nie moim dziełem):

def chunks(iterable, size):
    from itertools import chain, islice
    iterator = iter(iterable)
    for first in iterator:
        yield list(chain([first], islice(iterator, size - 1)))
Yongwei Wu
źródło
4

Rozwiązanie dla Pythona 3.8, jeśli pracujesz z iteracjami, które nie definiują lenfunkcji i wyczerpują się:

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

Przykładowe użycie:

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]

Oczywiście można by go również zaimplementować bez operatora morsa.

Atra Azami
źródło
1
W obecnej wersji batcherakceptuje iterator, a nie iterowalny. Spowodowałoby to na przykład nieskończoną pętlę z listą. Prawdopodobnie iterator = iter(iterable)przed rozpoczęciem whilepętli powinna być linia .
Daniel Perez
2

To jest to, czego używam w moim projekcie. Obsługuje elementy iteracyjne lub listy tak wydajnie, jak to tylko możliwe.

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()]
Josh Smeaton
źródło
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]]
Atila Romero
źródło
Najlepsza jak dotąd odpowiedź, działa z każdą strukturą danych
Clément Prévost
1

To zadziała dla każdego iterowalnego.

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

To działałoby tak:

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

PS: To nie zadziała, jeśli iterowalność ma wartości None.

Trideep Rath
źródło
1

Oto podejście wykorzystujące reducefunkcję.

Oneliner:

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

Lub bardziej czytelna wersja:

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

Test:

>>> 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]]
Lycha
źródło
0

Możesz po prostu pogrupować iterowalne elementy według ich indeksu wsadowego.

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

Często jest tak, gdy chcesz zebrać wewnętrzne iterowalne, więc tutaj jest bardziej zaawansowana wersja.

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

Przykłady:

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]]
dimathe47
źródło
0

Powiązane funkcje, których możesz potrzebować:

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

Stosowanie:

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

Pobiera i-tą partię z sekwencji i może również współpracować z innymi strukturami danych, takimi jak pandas dataframes ( df.iloc[batch(100,0)]) lub numpy array ( array[batch(100,0)]).

alvitawa
źródło
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,)]
yacc143
źródło
0

używam

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)]
  
gazorpazorp
źródło
0

Bierz (najwyżej) n elementów, aż się wyczerpie.

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
W. Zhu
źródło