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]


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.


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

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)

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.


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]

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

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.


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

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

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.


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

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

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


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

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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.