Що є найбільш "пітонічним" способом перегляду списку в шматках?


487

У мене є скрипт Python, який приймає в якості списку цілих чисел, з якими мені потрібно працювати з чотирма цілими числами одночасно. На жаль, я не маю контролю над входом, або я б передавав це як список чотирьохелементних кортежів. Наразі я повторюю це таким чином:

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]

Схоже, це дуже схоже на "C-think", що змушує мене підозрювати, що існує більш пітонічний спосіб вирішення цієї ситуації. Список викидається після ітерації, тому його не потрібно зберігати. Можливо, щось подібне було б краще?

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

Але все ще не дуже "добре", хоча. : - /

Питання, пов’язані з цим: як ви розділите список на рівні розміри в Python?


3
Ваш код не працює, якщо розмір списку не кратний чотирьом.
Педро Анрікес

5
Я розширюю список (), щоб його довжина була кратною чотирма, перш ніж дістатись так далеко.
Бен Бланк

4
@ ΤΖΩΤΖΙΟΥ - Питання дуже схожі, але не зовсім повторювані. Він "розділений на будь-яку кількість шматочків розміром N" проти "розбиття на N шматок будь-якого розміру". :-)
Бен Бланк


Відповіді:


339

Змінено в розділі рецептів документів itertools Python :

from itertools import zip_longest

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

Приклад
У псевдокоді, щоб тримати приклад коротко.

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

Примітка: на Python 2 використовуйте izip_longestзамість zip_longest.


67
Нарешті отримав шанс пограти з цим у сеансі пітона. Для тих, хто такий заплутаний, як і я, це подача того ж ітератора в izip_lolong кілька разів, внаслідок чого він споживає послідовні значення тієї ж послідовності, а не смугасті значення з окремих послідовностей. Я це люблю!
Бен Бланк

6
Який найкращий спосіб відфільтрувати заповнене значення? ([елемент для елемента в елементах, якщо елемент не заповнює значення] для елементів у групі (ітерабельний))?
gotgenes

14
Я підозрюю, що продуктивність цього рецепта групування для шматок розміром 256k буде дуже поганою, оскільки izip_longestбуде подано 256k аргументів.
anatoly techtonik

13
У кількох місцях коментатори кажуть "коли я нарешті розробив, як це працювало ...." Можливо, потрібно трохи пояснень. Зокрема, перелік ітераторів.
LondonRob

6
Чи є спосіб використати це, але без Noneзаповнення останньої частини?
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)

Простий. Легко. Швидкий. Працює з будь-якою послідовністю:

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
@Carlos Crasborn версія працює для будь-яких ітерабельних (не лише послідовностей, як наведений вище код); це стисло і, ймовірно, так само швидко або навіть швидше. Хоча це може бути дещо незрозумілим (незрозумілим) для людей, незнайомих з itertoolsмодулем.
jfs

1
Домовились. Це найбільш загальний і пітонічний спосіб. Чіткий і стислий. (і працює на двигуні додатків)
Метт Вільямсон

3
Зауважте, що chunkerповертається a generator. Замініть повернення на: return [...]щоб отримати список.
Dror

11
Замість того , щоб писати функції будівлі , а потім повертає генератор, ви також можете написати генератор безпосередньо, використовуючи yield: for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]. Я не впевнений, чи внутрішньо це було б вирішено по-різному в будь-якому відповідному аспекті, але це може бути навіть крихітним зрозумілішим.
Алфе

3
Зверніть увагу, це працює лише для послідовностей, які підтримують доступ до елементів за індексом і не працюватимуть для загальних ітераторів, оскільки вони можуть не підтримувати __getitem__метод.
аполлов

135

Я фанат

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

Як він поводиться, якщо len (ints) не є кратним розміром chunkSize?
PlsWork

3
@AnnaVopureta chunkматиме 1, 2 або 3 елементи для останньої партії елементів. Дивіться це питання про те, чому індекси фрагментів можуть бути поза межами .
Борис

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)

Інший спосіб:

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 за використання генераторів, шви як найбільш "пітонічні" з усіх запропонованих рішень
Сергій Головченко

7
Це досить довго і незграбно для чогось такого легкого, що зовсім не пітонічно. Я віддаю перевагу версії С. Лотта
zenazn

4
@zenazn: це працюватиме на екземплярах генератора, нарізання не буде
Janus Troelsen

Окрім належної роботи з генераторами та іншими нерозрізаними ітераторами, перше рішення також не вимагає значення "наповнювача", якщо кінцевий шматок менший, ніж sizeіноді бажано.
Дано

1
Також +1 для генераторів. Інші рішення вимагають lenдзвінка, тому не працюйте з іншими генераторами.
Куадуе


11

Ідеальним рішенням цієї проблеми є робота з ітераторами (не лише послідовностями). Він також повинен бути швидким.

Це рішення, передбачене документацією для 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)

Використовуючи ipython в %timeitмоєму Mac-ефірі, я отримую 47,5 нас за цикл.

Однак це дійсно не працює для мене, оскільки результати забиті для рівних груп. Рішення без прокладки трохи складніше. Найнаївнішим рішенням може бути:

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

Просто, але досить повільно: 693 us за цикл

Найкраще рішення, яке я міг би використати isliceдля внутрішнього циклу:

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

З тим самим набором даних я отримую 305 нас за цикл.

Неможливо отримати чисте рішення швидше, я надаю наступне рішення з важливим застереженням: Якщо у ваших вхідних даних є примірники filldata, ви можете отримати неправильну відповідь.

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

Мені справді не подобається ця відповідь, але це значно швидше. 124 нас за цикл


Можна зменшити час виконання для рецепта # 3 на ~ 10-15%, переміщаючи його до шару C (минаючи itertoolsімпорт, mapповинні бути PY3 mapабо imap): def grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n))))). Ваша остаточна функція може бути менш крихкою, використовуючи дозорний: позбудьтесь fillvalueаргументу; додайте перший рядок fillvalue = object(), потім змініть ifчек if i[-1] is fillvalue:і рядок, яким він керує yield tuple(v for v in i if v is not fillvalue). Гарантії, які не мають значення, не iterableможуть бути помилково прийняті за значення наповнювача.
ShadowRanger

До речі, великі пальці вгору на №4. Я збирався розмістити свою оптимізацію №3 як кращу відповідь (ефективніше), ніж те, що було розміщено дотепер, але з налаштуванням, щоб зробити її надійною, пружною №4 працює вдвічі швидше, ніж оптимізовано №3; Я не очікував, що рішення з петлями рівня Python (і відсутність теоретичних алгоритмічних відмінностей AFAICT) переможе. Я припускаю, що №3 програє через рахунок побудови / повторення isliceоб'єктів (№3 виграє, якщо nвідносно велика, наприклад, кількість груп невелика, але це оптимізація для нечастого випадку), але я не очікував, що це буде зовсім так крайній.
ShadowRanger

Для №4 перша гілка умовного лише коли-небудь береться за останню ітерацію (остаточний кортеж). Замість відтворення остаточного кортежу знову, кешувати по модулю довжини оригіналу ітерації у верхній частині і використовувати, щоб відрізати небажане доповнення від izip_longestна кінцевому наборі: yield i[:modulo]. Крім того , для argsзмінної, кортеж його замість списку: args = (iter(iterable),) * n. Голить ще кілька циклів годин. Нарешті, якщо ми проігноруємо fillvalue і припустимо None, що умовне може стати if None in iще більшим тактовим циклом.
Кумба

1
@Kumba: Ваша перша пропозиція передбачає, що введення має відому тривалість. Якщо це ітератор / генератор, а не колекція з відомою довжиною, немає чого кешувати. Немає жодної реальної причини використовувати таку оптимізацію; ви оптимізуєте нечастий випадок (останній yield), хоча звичайний випадок не впливає.
ShadowRanger

10

Мені потрібно було рішення, яке також працюватиме з наборами та генераторами. Я не міг придумати нічого дуже короткого і гарного, але це принаймні читабельно.

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

Список:

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

Набір:

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

Генератор:

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

8

Подібно до інших пропозицій, але не зовсім однакових, мені подобається робити це так, тому що це легко і легко читати:

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)

Таким чином, ви не отримаєте останній частковий шматок. Якщо ви хочете отримати (9, None, None, None)як останній шматок, просто скористайтеся izip_longestз itertools.


можна вдосконалитиzip(*([it]*4))
Жан-Франсуа Фабре

@ Жан-Франсуа Фабре: з точки зору читабельності я не вважаю це вдосконаленням. І це також незначно повільніше. Це покращення, якщо ти займаєшся гольфом, чого я не є.
kriss

ні, я не гольфу, але що робити, якщо у вас є 10 аргументів? Я прочитав цю конструкцію на якійсь офіційній сторінці. Але, звичайно, зараз не можу знайти її :)
Жан-Франсуа Фабре

@ Жан-Франсуа Фабре: якщо у мене є 10 аргументів або змінна кількість аргументів, це варіант, але я краще напишу: zip (* (it,) * 10)
kriss

правильно! ось що я читав. не те, що я склав :)
Жан-Франсуа Фабре

8

Якщо ви не заперечуєте проти використання зовнішнього пакету, ви можете використовувати його iteration_utilities.grouperз 1 . Він підтримує всі ітерабелі (не лише послідовності):iteration_utilties

from iteration_utilities import grouper
seq = list(range(20))
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, 17, 18, 19)

У разі, якщо довжина не кратна групі, вона також підтримує заповнення (неповна остання група) або обрізання (відкидання неповної останньої групи) останньої:

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)

Орієнтири

Я також вирішив порівняти час виконання декількох згаданих підходів. Це графік журналу журналів, що групується у групи елементів "10" на основі списку різної величини. Для якісних результатів: нижчий означає швидше:

введіть тут опис зображення

Принаймні, в цьому орієнтирі iteration_utilities.grouperнайкращі результати. Слідом за підходом Краз .

Тест створено за допомогою 1 . Код, який використовується для запуску цього еталону, був: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 Відмова: Я автор бібліотек iteration_utilitiesта simple_benchmark.


7

Оскільки це ще ніхто не згадував, ось таке zip()рішення:

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

Він працює лише в тому випадку, якщо довжина вашої послідовності завжди розділяється на розмір шматка або вам не байдуже про те, що воно є, якщо це не так.

Приклад:

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

Або за допомогою itertools.izip повернути ітератор замість списку:

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

Прокладку можна виправити за допомогою відповіді @ ΤΖΩΤΖΙΟΥ :

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

Використання map () замість zip () виправляє проблему прокладки у відповіді Дж. Ф. Себастьяна:

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

Приклад:

>>> 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
Це краще вирішується за допомогою itertools.izip_longest(Py2) / itertools.zip_longest(Py3); це використання mapподвійно застаріло і недоступне в Py3 (ви не можете перейти Noneяк функцію картографування, і воно припиняється, коли вичерпається найкоротший ітерабельний, не найдовший; він не зменшується).
ShadowRanger

4

Іншим підходом було б використання форми двох аргументів iter:

from itertools import islice

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

Це можна легко адаптувати для використання накладки (це схоже на відповідь Маркуса Джардеро ):

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)

Їх можна навіть комбінувати для додаткової прокладки:

_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
краще, тому що у вас є можливість опустити прокладку!
n611x007

3

Якщо список великий, найефективнішим способом зробити це буде використання генератора:

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

(Я вважаю, що пропозиція про MizardX в itertools функціонально рівнозначна цьому.)
Роберт Россні

1
(Насправді, замислюючись, ні, я не. Itertools.islice повертає ітератор, але він не використовує існуючий.)
Роберт Россні

Це приємно і просто, але чомусь навіть без перетворення згорнути в 4-7 разів повільніше, ніж прийнятий метод iterable = range(100000000)chunksize
групування

Однак загалом я б рекомендував цей метод, оскільки прийнятий може бути надзвичайно повільним, коли перевірка останнього елемента повільна docs.python.org/3/library/itertools.html#itertools.zip_lo most
Валентас

3

Використання маленьких функцій і речей мені дуже не подобається; Я вважаю за краще просто використовувати скибочки:

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

приємно, але немає користі для невизначеного потоку, про який не було відомо len. ви можете зробити тест за допомогою itertools.repeatабо itertools.cycle.
n611x007

1
Крім того, з'їдає пам'ять через використання [...for...] розуміння списку для фізичного складання списку замість використання (...for...) генераторного виразу, який би просто піклувався про наступний елемент і
запасу

2

Щоб уникнути всіх перетворень до списку, import itertoolsвиконайте вказані нижче дії.

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

Виробляє:

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

Я перевірив, groupbyі він не перетворюється на список чи використанняlen тому я думаю (це) це затримає роздільну здатність кожного значення, поки воно не буде фактично використане. На жаль, жодна з доступних відповідей (наразі), здається, не пропонувала цього варіанту.

Очевидно, якщо вам потрібно обробити кожен елемент по черзі, вкладіть a для циклу на 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

Моя особлива зацікавленість у цьому полягала в необхідності споживання генератора для подання змін в пакеті до 1000 в API gmail:

    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)

Що робити, якщо список, який ви збираєте, - це щось інше, ніж послідовність висхідних цілих чисел?
PaulMcG

@PaulMcGuire см GroupBy ; задана функція для опису порядку, тоді елементами ітерабельного може бути що завгодно, правда?
Джон Мей

1
Так, я знайомий з групою. Але якщо в повідомленнях були букви "ABCDEFG", то groupby(messages, lambda x: x/3)ви давали б вам TypeError (за спробу розділити рядок на int), а не 3-літерне групування. Тепер, якщо ви це зробили, groupby(enumerate(messages), lambda x: x[0]/3)ви можете щось мати. Але ви цього не сказали у своєму дописі.
PaulMcG

2

З NumPy це просто:

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

вихід:

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

Якщо я щось не пропускаю, наступне просте рішення з виразами генератора не згадувалося. Мається на увазі, що відомі як розмір, так і кількість шматок (що часто буває), і що не потрібно ніяких оббивок:

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

У вашому другому методі я перейшов би до наступної групи з 4-х, виконавши це:

ints = ints[4:]

Однак я не проводив жодного вимірювання продуктивності, тому не знаю, який з них може бути більш ефективним.

Сказавши це, я зазвичай вибирав перший метод. Це не дуже, але це часто є наслідком взаємодії із зовнішнім світом.


1

Ще одна відповідь, переваги якої:

1) Легко зрозуміло
2) Працює на будь-яких ітерабельних, а не лише на послідовностях (деякі з вищезазначених відповідей задушаться у файлових ручках)
3) Не завантажує шматок у пам'ять відразу відразу
4) Не складає перелік посилань на довгий той самий ітератор в пам'яті
5) Немає прокладки значень заповнення в кінці списку

Попри це, я ще не приуротив це, щоб це було повільніше, ніж деякі більш розумні методи, і деякі переваги можуть бути неактуальними з огляду на використання.

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 

Оновлення:
пара недоліків через те, що внутрішня і зовнішня петлі витягують значення з одного ітератора:
1) продовження не працює так, як очікувалося у зовнішньому циклі - воно просто продовжується до наступного пункту, а не пропускає шматок . Однак це не здається проблемою, оскільки у зовнішньому циклі немає що перевірити.
2) перерва не працює, як очікувалося, у внутрішньому циклі - керування знову повернеться у внутрішній цикл із наступним елементом в ітераторі. Щоб пропустити цілі шматки, або загорніть внутрішній ітератор (ii вище) в кортеж, наприклад for c in tuple(ii), або встановіть прапор та вичерпайте ітератор.


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 це пропускає прокладки; ваш і bcoughlan 's дуже схожий
n611x007

1

Ви можете використовувати розділ або скибки функціонувати з funcy бібліотеки:

from funcy import partition

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

Ці функції також мають версії ітератора ipartitionі ichunks, що буде ефективнішим у цьому випадку.

Ви також можете зазирнути до їх виконання .


1

Про рішення, дане J.F. Sebastian тут :

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

Це розумно, але має один недолік - завжди повертай кортеж. Як отримати натомість рядок?
Звичайно, можна написати''.join(chunker(...)) , але тимчасовий кортеж будується все одно.

Ви можете позбутися від тимчасового кортежу, написавши власне 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

Тоді

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

Приклад використання:

>>> 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
Не критика, призначена для вас, щоб змінити свою відповідь, а скоріше зауваження: Кодекс - це відповідальність. Чим більше коду ви пишете, тим більше місця ви створюєте для помилок, щоб приховати. З цієї точки зору, переписування zipзамість використання існуючого здається не найкращою ідеєю.
Алфе

1

Мені подобається такий підхід. Він здається простим і не магічним, підтримує всі видимі види і не потребує імпорту.

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

Я ніколи не хочу, щоб мої шматки були підбиті, тому ця вимога є важливою. Я вважаю, що вміння працювати над будь-яким ітерабельним також є необхідною умовою. Враховуючи це, я вирішив поширитись на прийняту відповідь https://stackoverflow.com/a/434411/1074659 .

У цьому підході продуктивність незначно вражає, якщо прокладки не потрібні через необхідність порівняння та фільтрації прокладених значень. Однак для великих розмірів шматок ця утиліта є дуже ефективною.

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

Ось фрагмент без імпорту, який підтримує генератори:

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

Приклад використання:

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

З Python 3.8 ви можете використовувати оператор моржів та 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

Здається, це не дуже сильно це зробити. Ось сторінка, яка має ряд методів, серед яких:

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

Якщо списки однакового розміру, ви можете об'єднати їх у списки 4-х кортезів zip(). Наприклад:

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

Ось що zip()виробляє функція:

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

Якщо списки великі, і ви не хочете поєднувати їх у більший список, використовуйте itertools.izip(), що створює ітератор, а не список.

from itertools import izip

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

0

Однолінійне рішення adhoc для повторення списку xв шматки розміру 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 ...
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.