Jaki jest najbardziej „pytonowy” sposób na iterację po liście w częściach?


487

Mam skrypt w języku Python, który przyjmuje jako dane wejściowe listę liczb całkowitych, które muszę obsługiwać jednocześnie z czterema liczbami całkowitymi. Niestety nie mam kontroli nad danymi wejściowymi lub przekazałbym je jako listę krotek czteroelementowych. Obecnie powtarzam to w ten sposób:

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]

Wygląda jednak podobnie do „C-think”, co sprawia, że ​​podejrzewam, że istnieje bardziej pythonowy sposób radzenia sobie z tą sytuacją. Lista jest odrzucana po iteracji, więc nie trzeba jej zachowywać. Być może coś takiego byłoby lepsze?

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

Nadal jednak nie „wydaje się” właściwe. : - /

Powiązane pytanie: Jak podzielić listę na kawałki o jednakowej wielkości w Pythonie?


3
Twój kod nie działa, jeśli rozmiar listy nie jest wielokrotnością czterech.
Pedro Henriques,

5
Rozszerzam () listę, aby jej długość była wielokrotnością czterech, zanim dotrze tak daleko.
Ben Blank

4
@ ΤΖΩΤΖΙΟΥ - pytania są bardzo podobne, ale nie do końca powtarzają się. Jest „podzielony na dowolną liczbę kawałków o rozmiarze N” vs. „podzielony na N kawałków o dowolnym rozmiarze”. :-)
Ben Blank


Odpowiedzi:


339

Zmodyfikowano z sekcji przepisów w dokumentacji itertools Pythona :

from itertools import zip_longest

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

Przykład
W pseudokodzie, aby zachować przykładowy zwięzły.

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

Uwaga: w Pythonie 2 użyj izip_longestzamiast zip_longest.


67
W końcu miałem okazję się z tym bawić w sesji Pythona. Dla tych, którzy są tak zdezorientowani jak ja, zasila to ten sam iterator izip_longest wiele razy, powodując, że zużywa on kolejne wartości tej samej sekwencji zamiast wartości rozłożonych z oddzielnych sekwencji. Kocham to!
Ben Blank

6
Jaki jest najlepszy sposób na odfiltrowanie wartości wypełnienia? ([pozycja dla pozycji w przedmiotach, jeśli pozycja nie jest wartością wypełnienia] dla pozycji w grupie (iterowalna))?
gotgenes

14
Podejrzewam, że wydajność tego przepisu dla grup wielkości 256k będzie bardzo słaba, ponieważ izip_longestotrzyma 256k argumentów.
anatoly techtonik

13
W kilku miejscach komentatorzy mówią „kiedy w końcu doszedłem do tego, jak to działa…” Może potrzebne jest trochę wyjaśnienia. W szczególności lista aspektów iteratorów.
LondonRob

6
Czy istnieje sposób, aby tego użyć, ale bez Nonewypełniania ostatniego kawałka?
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)

Prosty. Łatwy. Szybki. Działa z dowolną sekwencją:

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
Wersja @Carlos Crasborn działa dla każdej iterowalnej (nie tylko sekwencji jak powyższy kod); jest zwięzły i prawdopodobnie równie szybki lub nawet szybszy. Chociaż może to być nieco niejasne (niejasne) dla osób niezaznajomionych z itertoolsmodułem.
jfs

1
Zgoda. Jest to najbardziej ogólny i pytonowy sposób. Jasne i zwięzłe. (i działa na silniku aplikacji)
Matt Williamson,

3
Zauważ, że chunkerzwraca a generator. Zamień powrót na: return [...]aby uzyskać listę.
Dror

11
Zamiast pisać budynek funkcji, a następnie powrót do generatora, można również napisać generator bezpośrednio, przy użyciu yield: for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]. Nie jestem pewien, czy wewnętrznie byłoby to potraktowane inaczej w jakimkolwiek istotnym aspekcie, ale może być nawet odrobinę jaśniejsze.
Alfe,

3
Uwaga: działa to tylko w przypadku sekwencji, które obsługują dostęp do elementów według indeksu i nie będą działać dla ogólnych iteratorów, ponieważ mogą nie obsługiwać __getitem__metody.
apollov

135

Jestem fanem

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

Jak się zachowuje, jeśli len (ints) nie jest wielokrotnością chunkSize?
PlsWork

3
@AnnaVopureta chunkbędzie miała 1, 2 lub 3 elementy dla ostatniej partii elementów. Zobacz pytanie dotyczące tego, dlaczego wskaźniki wycinków mogą być poza zakresem .
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)

Inny sposób:

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 za korzystanie z generatorów, szwy jak najbardziej „pytoniczne” ze wszystkich sugerowanych rozwiązań
Sergey Golovchenko

7
Jest dość długi i niezgrabny jak na coś tak łatwego, co wcale nie jest bardzo pytoniczne. Wolę wersję S. Lott
zenazn

4
@zenazn: to zadziała na instancjach generatora, krojenie nie
Janus Troelsen

Oprócz poprawnej pracy z generatorami i innymi iteratorami nie do krojenia, pierwsze rozwiązanie również nie wymaga wartości „wypełniacza”, jeśli końcowa porcja jest mniejsza niż size, co jest czasem pożądane.
dano

1
Również +1 dla generatorów. Inne rozwiązania wymagają lenpołączenia, więc nie działają na innych generatorach.
Cuadue,


11

Idealne rozwiązanie tego problemu działa z iteratorami (nie tylko sekwencjami). Powinno być również szybkie.

Oto rozwiązanie dostarczone przez dokumentację dla 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)

Używając ipython %timeitna moim Mac Book Air, otrzymuję 47,5 us na pętlę.

Jednak to naprawdę nie działa dla mnie, ponieważ wyniki są wypełnione grupami o równej wielkości. Rozwiązanie bez wypełnienia jest nieco bardziej skomplikowane. Najbardziej naiwnym rozwiązaniem może być:

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

Proste, ale dość wolne: 693 us na pętlę

Najlepsze rozwiązanie, jakie mogłem wymyślić z wykorzystaniem islicepętli wewnętrznej:

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

Przy tym samym zestawie danych otrzymuję 305 us na pętlę.

Nie jestem w stanie uzyskać czystego rozwiązania w jakikolwiek sposób, dlatego przedstawiam następujące rozwiązanie z ważnym zastrzeżeniem: Jeśli twoje dane wejściowe zawierają w sobie instancje filldata, możesz uzyskać błędną odpowiedź.

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

Naprawdę nie lubię tej odpowiedzi, ale jest ona znacznie szybsza. 124 us na pętlę


Można zmniejszyć czas pracy receptury # 3 przez ~ 10-15%, przesuwając go do warstwy C (z pominięciem itertoolsimportu; mapmusi być Py3 maplub imap) def grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n))))). Twoja ostatnia funkcja może być mniej krucha za pomocą wartownika: pozbyć się fillvalueargumentu; dodaj pierwszy wiersz fillvalue = object(), a następnie zmień pole ifwyboru if i[-1] is fillvalue:i linię, którą kontroluje yield tuple(v for v in i if v is not fillvalue). Gwarantuje, że żadnej wartości nie iterablemożna pomylić z wartością wypełniacza.
ShadowRanger

BTW, wielkie kciuki do góry na # 4. Już miałem opublikować moją optymalizację nr 3 jako lepszą odpowiedź (pod względem wydajności) niż to, co zostało opublikowane do tej pory, ale z poprawką, aby była niezawodna, odporna # 4 działa dwa razy szybciej niż zoptymalizowana # 3; Nie spodziewałem się, że wygra rozwiązanie z pętlami poziomu Pythona (i bez teoretycznych różnic algorytmicznych AFAICT). Zakładam, że # 3 przegrywa z powodu kosztów budowy / iteracji isliceobiektów (# 3 wygrywa, jeśli njest stosunkowo duży, np. Liczba grup jest niewielka, ale optymalizuje się w rzadkich przypadkach), ale nie spodziewałem się, że będzie to całkiem skrajny.
ShadowRanger

W przypadku # 4 pierwsza gałąź warunkowa jest pobierana tylko podczas ostatniej iteracji (ostatecznej krotki). Zamiast całego ponownie odtworzenie ostateczną krotki, buforować modulo długości oryginału iterable na górze i używać, aby odciąć niechcianych wyściółką z izip_longestnad ostatecznym krotki: yield i[:modulo]. Ponadto, dla argszmiennej, krotka go zamiast listy: args = (iter(iterable),) * n. Goli kilka kolejnych cykli zegara. Wreszcie, jeśli zignorujemy wartość wypełnienia i założymy None, warunek może stać się if None in idla jeszcze większej liczby cykli zegara.
Kumba

1
@Kumba: Twoja pierwsza sugestia zakłada, że ​​dane wejściowe mają znaną długość. Jeśli jest to iterator / generator, a nie kolekcja o znanej długości, nie ma nic do buforowania. Zresztą nie ma żadnego powodu, aby korzystać z takiej optymalizacji; optymalizujesz przypadek rzadki (ostatni yield), podczas gdy zwykły przypadek pozostaje niezmieniony.
ShadowRanger,

10

Potrzebowałem rozwiązania, które działałoby również z zestawami i generatorami. Nie mogłem wymyślić niczego bardzo krótkiego i ładnego, ale przynajmniej jest to dość czytelne.

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

Lista:

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

Zestaw:

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

Podobnie jak inne propozycje, ale nie do końca identyczne, lubię to robić w ten sposób, ponieważ jest prosty i łatwy do odczytania:

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)

W ten sposób nie dostaniesz ostatniej częściowej części. Jeśli chcesz dostać się (9, None, None, None)jako ostatni kawałek, po prostu użyj izip_longestz itertools.


można ulepszyć za pomocązip(*([it]*4))
Jean-François Fabre

@ Jean-François Fabre: z punktu widzenia czytelności nie widzę w tym poprawy. Jest także nieco wolniejszy. To poprawa, jeśli grasz w golfa, a ja nie.
kriss

nie, nie gram w golfa, ale co, jeśli masz 10 argumentów? Przeczytałem ten konstrukt na jakiejś oficjalnej stronie. Ale oczywiście nie mogę go teraz znaleźć :)
Jean-François Fabre

@ Jean-François Fabre: jeśli mam 10 argumentów lub zmienną liczbę argumentów, jest to opcja, ale wolałbym napisać: zip (* (it,) * 10)
kriss

dobrze! tak czytam. nie lista rzeczy, które wymyśliłem :)
Jean-François Fabre

8

Jeśli nie masz nic przeciwko użyciu zewnętrznego pakietu, możesz użyć iteration_utilities.grouperod 1 . Obsługuje wszystkie iterowalne (nie tylko sekwencje):iteration_utilties

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

który drukuje:

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

Jeśli długość nie jest wielokrotnością rozmiaru grupy, obsługuje także wypełnianie (niekompletna ostatnia grupa) lub obcinanie (odrzucanie niekompletnej ostatniej grupy) ostatniej:

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)

Benchmarki

Postanowiłem także porównać czas działania kilku wymienionych podejść. Jest to wykres dziennika-dziennika grupujący w grupy elementów „10” na podstawie listy o różnej wielkości. Dla wyników jakościowych: Niższy oznacza szybszy:

wprowadź opis zdjęcia tutaj

Przynajmniej w tym teście iteration_utilities.grouperdziała najlepiej. Następnie następuje podejście Craza .

Benchmark został stworzony przy użyciu 1 . Kod użyty do uruchomienia tego testu porównawczego to: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 Zastrzeżenie: Jestem autorem bibliotek iteration_utilitiesi simple_benchmark.


7

Ponieważ nikt o tym nie wspomniał, oto zip()rozwiązanie:

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

Działa tylko wtedy, gdy długość sekwencji jest zawsze podzielna przez rozmiar porcji lub jeśli nie obchodzi cię końcowy fragment, jeśli tak nie jest.

Przykład:

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

Lub użyj itertools.izip, aby zwrócić iterator zamiast listy:

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

Wypełnienie można naprawić za pomocą odpowiedzi @ ΤΖΩΤΖΙΟΥ :

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

Użycie map () zamiast zip () rozwiązuje problem dopełniania w odpowiedzi JF Sebastiana:

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

Przykład:

>>> 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
Jest to lepiej obsługiwane przy pomocy itertools.izip_longest(Py2) / itertools.zip_longest(Py3); to użycie mapjest podwójnie przestarzałe i niedostępne w Py3 (nie można przejść Nonejako funkcja odwzorowująca, i zatrzymuje się, gdy wyczerpana zostanie najkrótsza iteracja, a nie najdłuższa; nie padnie).
ShadowRanger

4

Innym podejściem byłoby użycie dwuargumentowej formy iter:

from itertools import islice

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

Można to łatwo dostosować do użycia paddingu (jest to podobne do odpowiedzi Markusa Jarderota ):

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)

Można je nawet połączyć w celu opcjonalnego wypełnienia:

_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
lepiej, ponieważ masz możliwość pominięcia wypełnienia!
n611x007

3

Jeśli lista jest duża, najskuteczniejszym sposobem na to będzie użycie generatora:

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

(Myślę, że sugestia MizardXa jest funkcjonalnie równoważna z tym.)
Robert Rossney,

1
(Właściwie, po zastanowieniu, nie, ja nie. Itertools.islice zwraca iterator, ale nie używa już istniejącego.)
Robert Rossney

Jest ładny i prosty, ale z jakiegoś powodu nawet bez konwersji na krotkę 4-7 razy wolniej niż akceptowana metoda grupowania na iterable = range(100000000)i chunksizedo 10000.
Valentas

Jednak ogólnie polecam tę metodę, ponieważ zaakceptowana metoda może być bardzo wolna, gdy sprawdzanie ostatniego elementu jest powolne docs.python.org/3/library/itertools.html#itertools.zip_longest
Valentas

3

Używanie małych funkcji i rzeczy tak naprawdę nie podoba mi się; Wolę po prostu użyć plasterków:

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

ładne, ale nie dobre dla nieokreślonego strumienia, który nie jest znany len. możesz zrobić test za pomocą itertools.repeatlub itertools.cycle.
n611x007

1
Również zjada pamięci, ponieważ wynikające z zastosowania [...for...] listowych fizycznie zbudować listę zamiast użycia (...for...) wyrażenia generatora , które po prostu dbają o kolejnym elemencie i pamięci wolnym
n611x007

2

Aby uniknąć wszystkich konwersji do listy import itertoolsi:

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

Produkuje:

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

sprawdziłem groupby i nie można przekonwertować na listę ani użyćlen więc (sądzę) opóźni to rozdzielczość każdej wartości, dopóki nie zostanie faktycznie użyta. Niestety, żadna z dostępnych odpowiedzi (w tym czasie) nie oferowała tej odmiany.

Oczywiście, jeśli chcesz obsłużyć każdy element z kolei, zagnieżdż pętlę for nad 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

Moim szczególnym zainteresowaniem była potrzeba użycia generatora, aby przesłać zmiany w partiach do 1000 do interfejsu API Gmaila:

    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)

Co zrobić, jeśli tworzona lista jest czymś innym niż sekwencja rosnących liczb całkowitych?
PaulMcG

@PaulMcGuire patrz groupby ; biorąc pod uwagę funkcję opisującą porządek, elementy iterowalne mogą być czymkolwiek, prawda?
John Mee,

1
Tak, znam Groupby. Ale jeśli wiadomości byłyby literami „ABCDEFG”, to groupby(messages, lambda x: x/3)dałby ci TypeError (za próbę podzielenia ciągu przez liczbę całkowitą), a nie 3-literowe grupowanie. Jeśli tak, groupby(enumerate(messages), lambda x: x[0]/3)możesz coś mieć. Ale nie powiedziałeś tego w swoim poście.
PaulMcG,

2

Z NumPy jest to proste:

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

wynik:

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

O ile mi czegoś nie brakuje, nie wspomniano o prostym rozwiązaniu z wyrażeniami generatora. Zakłada, że zarówno rozmiar, jak i liczba porcji są znane (co często ma miejsce) i że nie jest wymagane wypełnianie:

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

W drugiej metodzie przejdę do następnej grupy 4, wykonując następujące czynności:

ints = ints[4:]

Nie wykonałem jednak żadnego pomiaru wydajności, więc nie wiem, który z nich może być bardziej wydajny.

Powiedziawszy to, zwykle wybrałbym pierwszą metodę. To nie jest ładne, ale często jest to konsekwencja kontaktu ze światem zewnętrznym.


1

Kolejna odpowiedź, której zaletami są:

1) Łatwo zrozumiałe
2) Działa na dowolnych iterowalnych, nie tylko sekwencjach (niektóre z powyższych odpowiedzi dławią się na uchwytach plików)
3) Nie ładuje porcji do pamięci naraz
4) Nie tworzy obszernej listy odniesień do ten sam iterator w pamięci
5) Brak dopełniania wartości wypełnienia na końcu listy

To powiedziawszy, nie zaplanowałem tego, więc może być wolniejsze niż niektóre bardziej sprytne metody, a niektóre zalety mogą być nieistotne w przypadku użycia.

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 

Aktualizacja:
Kilka wad związanych z faktem, że wewnętrzne i zewnętrzne pętle pobierają wartości z tego samego iteratora:
1) kontynuacja nie działa zgodnie z oczekiwaniami w zewnętrznej pętli - po prostu przechodzi do następnego elementu zamiast pomijania fragmentu . Nie wydaje się to jednak problemem, ponieważ nie ma nic do przetestowania w zewnętrznej pętli.
2) przerwa nie działa zgodnie z oczekiwaniami w wewnętrznej pętli - kontrola ponownie skończy w wewnętrznej pętli z kolejnym elementem w iteratorze. Aby pominąć całe fragmenty, albo zawiń wewnętrzny iterator (ii powyżej) w krotkę, np. for c in tuple(ii), Lub ustaw flagę i wyczerpaj 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 pomija dopełnienie; twoje i bcoughlan 's są bardzo podobne
n611x007

1

Możesz użyć funkcji partycji lub porcji z biblioteki funcy :

from funcy import partition

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

Funkcje te mają również wersje iteratora ipartitioni ichunks, które w tym przypadku będą bardziej wydajne.

Możesz także zerknąć na ich implementację .


1

O rozwiązaniu podanym J.F. Sebastian tutaj :

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

Jest sprytny, ale ma jedną wadę - zawsze wraca krotka. Jak zamiast tego uzyskać ciąg?
Oczywiście, że możesz pisać''.join(chunker(...)) , ale tymczasowa krotka i tak jest skonstruowana.

Możesz pozbyć się tymczasowej krotki, pisząc własną zip, w ten sposób:

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

Następnie

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

Przykładowe użycie:

>>> 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
Nie krytyka przeznaczona do zmiany odpowiedzi, ale komentarz: Kod to zobowiązanie. Im więcej kodu napiszesz, tym więcej miejsca tworzysz dla błędów do ukrycia. Z tego punktu widzenia przepisywanie zipzamiast korzystania z istniejącego nie wydaje się najlepszym pomysłem.
Alfe

1

Lubię to podejście. Jest prosty i nie magiczny, obsługuje wszystkie typy iteracyjne i nie wymaga importu.

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

Nigdy nie chcę, aby moje kawałki były wyściełane, więc ten wymóg jest niezbędny. Uważam, że umiejętność pracy nad dowolnym iterowalnym jest również wymagana. Biorąc to pod uwagę, zdecydowałem się rozszerzyć na zaakceptowaną odpowiedź, https://stackoverflow.com/a/434411/1074659 .

W przypadku tego podejścia wydajność jest niewielka, jeśli wypełnienie nie jest pożądane ze względu na konieczność porównania i filtrowania wartości wypełnienia. Jednak w przypadku dużych porcji narzędzie to jest bardzo wydajne.

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

Oto porcja bez importu, która obsługuje generatory:

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

Przykład zastosowania:

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

W Pythonie 3.8 możesz używać operatora morsa i 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

Wydaje się, że nie ma na to ładnego sposobu. Oto strona z wieloma metodami, w tym:

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

Jeśli listy mają ten sam rozmiar, możesz połączyć je w listy 4-krotne z zip(). Na przykład:

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

Oto, co zip()wytwarza funkcja:

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

Jeśli listy są duże i nie chcesz ich łączyć w większą listę, użyj opcji itertools.izip(), która tworzy iterator, a nie listę.

from itertools import izip

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

0

Jednoliniowe, adhoc rozwiązanie iteracji po liście xw kawałkach wielkości 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 ...
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.