Каков наиболее "питонический" способ перебора списка по частям?

У меня есть скрипт 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 разбить список на части одинакового размера?

Ваш код не работает, если размер списка не кратен четырем.

Pedro Henriques 12.01.2009 06:03

Я расширяю список так, чтобы его длина была кратна четырем, прежде чем он зашел так далеко.

Ben Blank 12.01.2009 06:44

@ ΤΖΩΤΖΙΟΥ - Вопросы очень похожи, но не повторяются. Он «разбивается на любое количество кусков размера N» по сравнению с «разбивается на N кусков любого размера». :-)

Ben Blank 21.07.2011 22:16
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
543
4
159 628
37
Перейти к ответу Данный вопрос помечен как решенный

Ответы 37

Кажется, нет красивого способа сделать это. Здесь - это страница, на которой есть несколько методов, в том числе:

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

Во втором методе я бы перешел к следующей группе из 4 человек, выполнив следующие действия:

ints = ints[4:]

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

Сказав это, я обычно выбираю первый метод. Это некрасиво, но это часто является следствием взаимодействия с внешним миром.

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

+1 за использование генераторов, швов как самое "питоническое" из всех предложенных решений

Sergey Golovchenko 12.01.2009 06:23

Он довольно длинный и неуклюжий для чего-то настолько простого, что совсем не питонично. Я предпочитаю версию С. Лотта

zenazn 12.01.2009 06:51

@zenazn: это будет работать на экземплярах генератора, нарезка не будет

Janus Troelsen 25.11.2012 21:33

В дополнение к правильной работе с генераторами и другими итераторами, не допускающими нарезки, первое решение также не требует значения «заполнителя», если конечный фрагмент меньше, чем size, что иногда желательно.

dano 20.08.2014 00:27

Также +1 для генераторов. Для других решений требуется вызов len, поэтому они не работают с другими генераторами.

Cuadue 10.04.2015 20:58

Я бы бросил try: block и перехватил исключение value error для обработки множественной проблемы <4.

Tom Myddeltyn 06.05.2016 01:16

Первая - хорошая, простая версия, не использующая fillvalue, но работающая на любых итерациях. Хороший!

Yuval 02.06.2017 10:18

Я фанат

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 18.02.2019 02:15

@AnnaVopureta chunk будет иметь 1, 2 или 3 элемента для последней партии элементов. См. Этот вопрос о том, почему индексы срезов могут быть вне пределов.

Boris 25.03.2019 21:56
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']

Версия @Carlos Crasborn работает с любыми итерациями (а не только с последовательностями, как в приведенном выше коде); он краткий и, вероятно, такой же быстрый или даже быстрее. Хотя это может быть немного непонятно (непонятно) для людей, незнакомых с модулем itertools.

jfs 12.01.2009 17:39

@ J.F. Себастьян - Теперь, когда у меня появилась возможность выяснить, работает ли Зачем его код, я чувствую себя обязанным изменить свой принятый ответ (что я делаю ненавидеть). Мне тоже нравится этот ответ, @nosklo, но этот трюк izip_longest кажется адаптированным для моей ситуации.

Ben Blank 13.01.2009 01:03

Согласовано. Это самый общий и питонический способ. Понятно и лаконично. (и работает на движке приложения)

Matt Williamson 08.08.2010 08:08

У меня были проблемы с его использованием, но он начал работать, когда я заменил внешние скобки квадратными скобками. Синтаксис в ответе только на Python 3?

RoboCop87 03.07.2014 19:41

Обратите внимание, что chunker возвращает generator. Замените возврат на: return [...], чтобы получить список.

Dror 24.02.2015 11:59

Вместо того, чтобы писать построение функции и затем возвращать генератор, вы также можете написать генератор напрямую, используя yield: for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]. Я не уверен, что внутренне это будет обрабатываться по-другому в каком-либо соответствующем аспекте, но это может быть даже немного яснее.

Alfe 15.04.2016 13:22

Обратите внимание, что это работает только для последовательностей, которые поддерживают доступ к элементам по индексу, и не будет работать для общих итераторов, поскольку они могут не поддерживать метод __getitem__.

apollov 22.12.2017 21:17

Я это переписал как генератор. Не могли бы вы сделать это? У него есть все преимущества, нет недостатков, и он более эффективен с точки зрения памяти.

smci 25.05.2019 09:47

@smci функция chunker() выше генератор - возвращает выражение генератора

nosklo 25.05.2019 14:11

@nosklo: Ну ладно, я его переписал как простой генератор: for i in range(0, len(seq), size): yield seq[i:i + size], который мне показался проще.

smci 25.05.2019 14:12

Мне это нравится! Его очень легко понять и преобразовать в генератор. +1

umbe1987 26.02.2021 18:05

Если список большой, наиболее эффективным способом сделать это будет использование генератора:

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

(Я думаю, что предложение itertools от MizardX функционально эквивалентно этому.)

Robert Rossney 12.01.2009 06:40

(На самом деле, подумав, нет. Itertools.islice возвращает итератор, но не использует существующий.)

Robert Rossney 12.01.2009 07:15

Это красиво и просто, но почему-то даже без преобразования в кортеж в 4-7 раз медленнее, чем принятый метод группировщика на iterable = range(100000000) и chunksize до 10000.

Valentas 08.10.2018 11:22

Однако в целом я бы рекомендовал этот метод, потому что принятый может быть очень медленным, когда проверка последнего элемента выполняется медленно docs.python.org/3/library/itertools.html#itertools.zip_longe‌ st

Valentas 08.10.2018 12:51

Если списки одинакового размера, вы можете объединить их в списки из 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):
    ...
from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)

Читаемый способ сделать это - stackoverflow.com/questions/434287/…

jfs 12.01.2009 17:29

Обратите внимание, что в python 3 izip_longest заменен на zip_longest

mdmjsh 10.10.2019 21:52
Ответ принят как подходящий

Изменено из раздела рецепты документации Python itertools:

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.

Наконец-то появился шанс поиграть с этим в сеансе Python. Для тех, кто так же смущен, как и я, это несколько раз подает один и тот же итератор в izip_longest, заставляя его использовать последовательные значения одной и той же последовательности, а не чередующиеся значения из отдельных последовательностей. Я люблю это!

Ben Blank 13.01.2009 01:00

Как лучше всего отфильтровать значение заполнения? ([элемент для элемента в элементах, если элемент не является значением заполнения] для элементов в группировщике (итерируемый))?

gotgenes 27.08.2009 02:48

Я не уверен, что это самый питонический ответ, но, возможно, это лучшее использование структуры [LIST]*n.

Utku Zihnioglu 15.02.2011 03:01

Это работает, но кажется, что это зависит от реализации интерпретатора. Гарантирует ли спецификация itertools.izip_longest чередующийся порядок доступа для итераторов (например, с 3 итераторами A, B и C порядок доступа будет A, B, C, A, B, C, A, Fill, C а не что-то вроде A, A, B, B, C, C, A, Fill, C или A, B, C, C, B, A, A, Fill, C? Я мог видеть, что последние порядки полезны для кеширования оптимизация производительности. Если упорядочение доступа с одним чередованием не гарантировано, это не является теоретически безопасным решением (хотя, говоря практически, большинство реализаций будут одношаговыми итераторами).

David B. 23.07.2011 09:59

Вы можете объединить все это в короткий однострочник: zip(*[iter(yourList)]*n) (или izip_longest с fillvalue)

ninjagecko 28.04.2012 18:55

Я подозреваю, что производительность этого рецепта группировщика для блоков размером 256 КБ будет очень низкой, потому что izip_longest будет загружать 256 КБ аргументов.

anatoly techtonik 28.04.2013 19:07

Что такое объект izip_longest, почему он не ведет себя как список и почему он возвращается из него? Почему я должен вызывать list(), почему он просто не возвращает новый список?

davidgoli 26.11.2013 06:31

@DavidB .: рецепт приведен в качестве примера кода в официальной документации. Если только это не ошибка; поведение гарантировано

jfs 24.04.2014 01:26

Чтобы быть эффективным с большим n, вам нужно будет управлять пулом и кормить его islice, как здесь github.com/Suor/funcy/blob/1.0.0/funcy/seqs.py#L293

Suor 05.06.2014 00:08

В нескольких местах комментаторы говорят: «Когда я наконец понял, как это работает…» Может потребоваться небольшое объяснение. В частности, аспект списка итераторов.

LondonRob 14.08.2015 10:00

Я предпочитаю размещать аргументы в порядке сначала размера, а затем последовательность. Это упрощает создание партиалов для разбиения на определенное количество, а затем просто передачу им различных последовательностей.

PaulMcG 17.10.2015 17:53

О производительности @anatolytechtonik судят по измерениям реальных данных на реальном оборудовании, а не по гипотетической «логике». За эти годы я много раз использовал рецепт grouper (). Я встречал случаи, когда вы не могли использовать нарезку, потому что входные данные не являются последовательностью. Я не помню ни одного случая, когда я бы заменил код, похожий на grouper (), на код, подобный chunker (), из-за проблем с производительностью (я мог бы заменить его для удобства чтения в коде для начинающих). YMMV

jfs 03.12.2016 02:37

@gotgenes, (filter(None, chunk) for chunk in zip_longest(*[iter(yourList)]*n) предоставит генератор фрагментов. Каждый блок сам по себе является генератором (с использованием фильтра), который пропускает значения заполнения.

flutefreak7 01.04.2018 10:40

если next () для исчерпанного итератора оказывается медленным (например, курсор psycopg2), последний фрагмент zip_longest в среднем n/2 раз медленнее, чем должен быть.

Valentas 08.10.2018 12:48

Есть ли способ использовать это, но без того, чтобы None заполнил последний кусок?

CMCDragonkai 11.12.2018 08:31

Это менее интенсивно / быстрее, чем кажется, по-видимому, потому что количество аргументов и итерация являются указателями на один и тот же объект (ы).

Gringo Suave 12.02.2020 23:45

@CMCDragonkai, вы можете отфильтровать итератор с помощью (x for x in chunk if x), предполагая, что ваше значение заполнения - None.

xbello 30.04.2020 15:33

Я бы не рекомендовал это, поскольку он основан на деталях реализации zip_longest. Даже если его поведение гарантировано, для чтения (и понимания) этого рецепта по-прежнему требуются знания, выходящие за рамки API. Однако интересно как упражнение на Python.

emazep 07.02.2021 07:45

Поскольку об этом еще никто не упоминал, вот решение для 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)

Использование 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')]

С этим лучше справиться с itertools.izip_longest (Py2) / itertools.zip_longest (Py3); такое использование map вдвойне устарело и недоступно в Py3 (вы не можете передать None в качестве функции сопоставления, и он останавливается, когда исчерпывается самая короткая итерация, а не самая длинная; она не заполняется).

ShadowRanger 01.10.2016 04:34

Идеальное решение этой проблемы работает с итераторами (а не только с последовательностями). Это также должно быть быстро.

Это решение, предоставленное документацией для 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 Book Air, я получаю 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 мкс за цикл

Лучшее решение, которое я мог придумать, использует 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 30.09.2016 04:14

Кстати, большие пальцы вверх на №4. Я собирался опубликовать свою оптимизацию № 3 как лучший ответ (с точки зрения производительности), чем то, что было опубликовано до сих пор, но с настройкой, делающей его надежным, отказоустойчивым № 4, работает в два раза быстрее, чем оптимизированный № 3; Я не ожидал, что решение с циклами уровня Python (и без теоретических алгоритмических различий AFAICT) победит. Я предполагаю, что №3 проигрывает из-за расходов на создание / повторение объектов islice (№3 выигрывает, если n относительно велик, например, количество групп невелико, но это оптимизация для необычного случая), но я не ожидал, что это будет совсем такая крайность.

ShadowRanger 30.09.2016 04:26

Для # 4 первая ветвь условного выражения всегда берется только на последней итерации (последний кортеж). Вместо того, чтобы заново воссоздавать окончательный кортеж, кешируйте модуль длины исходного итерируемого элемента вверху и используйте его, чтобы вырезать нежелательное заполнение из izip_longest в последнем кортеже: yield i[:modulo]. Кроме того, для переменной args используйте кортеж вместо списка: args = (iter(iterable),) * n. Сокращает еще несколько тактов. Наконец, если мы проигнорируем значение заполнения и примем None, условное выражение может стать if None in i для еще большего количества тактовых циклов.

Kumba 14.08.2017 23:55

@Kumba: ваше первое предложение предполагает, что длина ввода известна. Если это итератор / генератор, а не коллекция известной длины, кэшировать нечего. В любом случае нет реальной причины использовать такую ​​оптимизацию; вы оптимизируете необычный случай (последний yield), в то время как общий случай не изменяется.

ShadowRanger 13.11.2017 22:42

Еще один ответ, преимущества которого:

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) continue не работает должным образом во внешнем цикле - он просто переходит к следующему элементу, а не пропускает фрагмент. Однако это не похоже на проблему, поскольку во внешнем цикле нечего тестировать. 2) break не работает должным образом во внутреннем цикле - управление снова перейдет во внутренний цикл со следующим элементом в итераторе. Чтобы пропустить целые фрагменты, либо оберните внутренний итератор (ii выше) в кортеж, например for c in tuple(ii), или установить флаг и исчерпать итератор.

Подобно другим предложениям, но не совсем идентично, мне нравится делать это таким образом, потому что это просто и легко читается:

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

Jean-François Fabre 01.01.2019 01:32

@ Жан-Франсуа Фабр: с точки зрения удобочитаемости я не считаю это улучшением. И это также немного медленнее. Это улучшение, если ты играешь в гольф, а я нет.

kriss 02.01.2019 04:28

нет, я не играю в гольф, но что, если у вас есть 10 аргументов? Я читал эту конструкцию на какой-то официальной странице, но, конечно, не могу найти ее прямо сейчас :)

Jean-François Fabre 02.01.2019 12:13

@ Жан-Франсуа Фабр: если у меня 10 аргументов или переменное количество аргументов, это вариант, но я бы лучше написал: zip (* (it,) * 10)

kriss 02.01.2019 14:21

верно! это что я читал. не тот список, который я составил :)

Jean-François Fabre 02.01.2019 15:35

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

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 24.04.2014 13:57

Кроме того, съедает память из-за использования [...for...]понимание списка для физического построения списка вместо использования (...for...)генератор выражения, который будет заботиться только о следующем элементе и свободной памяти.

n611x007 24.04.2014 14:00

Мне нужно было решение, которое также работало бы с наборами и генераторами. Я не мог придумать ничего очень короткого и красивого, но, по крайней мере, это вполне читабельно.

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]]
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с очень похожи

n611x007 24.04.2014 13:54

Другой подход - использовать форму 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)

предпочтительнее, потому что у вас есть возможность опустить отступы!

n611x007 24.04.2014 13:50

Вы можете использовать функцию перегородка или куски из библиотеки веселье:

from funcy import partition

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

Эти функции также имеют версии итератора ipartition и ichunks, которые в этом случае будут более эффективными.

Вы также можете заглянуть в их реализация.

Однострочное специальное решение для перебора списка 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 ...

Чтобы избежать всех преобразований в список 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, поэтому я (думаю) это задержит разрешение каждого значения до тех пор, пока оно не будет фактически использовано. К сожалению, ни один из доступных ответов (на данный момент) не предлагал такой вариант.

Очевидно, что если вам нужно обрабатывать каждый элемент по очереди, вложите цикл for в 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 17.10.2015 17:33

@PaulMcGuire см. группа по; учитывая функцию для описания порядка, тогда элементы итерации могут быть любыми, не так ли?

John Mee 19.10.2015 07:55

Да, я знаком с groupby. Но если бы сообщения были буквами «ABCDEFG», то groupby(messages, lambda x: x/3) выдал бы вам TypeError (для попытки разделить строку на int), а не трехбуквенные группировки. Теперь, если вы сделали groupby(enumerate(messages), lambda x: x[0]/3), у вас может быть что-нибудь. Но вы не сказали этого в своем посте.

PaulMcG 20.10.2015 00:36

С 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

Сначала я спроектировал его для разделения строк на подстроки для анализа строки, содержащей шестнадцатеричный код. Сегодня превратил его в сложный, но простой генератор.

def chunker(iterable, size, reductor, condition):
    it = iter(iterable)
    def chunk_generator():
        return (next(it) for _ in range(size))
    chunk = reductor(chunk_generator())
    while condition(chunk):
        yield chunk
        chunk = reductor(chunk_generator())

Аргументы:

Очевидные

  • iterable - это любой итератор / итератор / генератор, продолжающий / генерирующий / повторяющий входные данные,
  • size - это, конечно, размер фрагмента, который вы хотите получить,

Интереснее

  • reductor - это вызываемый объект, который получает генератор, повторяющий содержимое блока. Я ожидал, что он вернет последовательность или строку, но я этого не требую.

    В качестве этого аргумента можно передать, например, list, tuple, set, frozenset,
    . или что-нибудь более интересное. Я бы передал эту функцию, вернув строку
    (при условии, что iterable содержит / генерирует / перебирает строки):

    def concatenate(iterable):
        return ''.join(iterable)
    

    Note that reductor can cause closing generator by raising exception.

  • condition - это вызываемый объект, который получает все, что вернул reductor. Он решает утвердить и уступить его (возвращая что-либо оценивающее в True),
    или отклонить его и завершить работу генератора (вернув что-нибудь другое или вызвав исключение).

    Когда количество элементов в iterable не делится на size, когда it исчерпывается, reductor получит генератор, генерирующий меньше элементов, чем size.
    Назовем эти элементы длится элементы.

    Я предложил передать в качестве аргумента две функции:

    • lambda x:x - будет выдан длится элементы.

    • lambda x: len(x)==<size> - длится элементы будет отклонен. replace <size> using number equal to size

По поводу решения, предоставленного 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'

Не критика, предназначенная для того, чтобы вы изменили свой ответ, а скорее комментарий: Код - это ответственность. Чем больше кода вы напишете, тем больше места вы создадите для скрытия ошибок. С этой точки зрения переписывание zip вместо использования существующего кажется не лучшей идеей.

Alfe 15.04.2016 13:32

Вот чанкер без импорта, который поддерживает генераторы:

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

itertools.groupby легко заставить работать, чтобы вы получали итерацию из итераций, без создания каких-либо временных списков:

groupby(iterable, (lambda x,y: (lambda z: x.next()/y))(count(),100))

Не пугайтесь вложенных лямбд, внешняя лямбда запускается только один раз, чтобы поместить генератор count() и константу 100 в область действия внутренней лямбды.

Я использую это для отправки кусков строк в mysql.

for k,v in groupby(bigdata, (lambda x,y: (lambda z: x.next()/y))(count(),100))):
    cursor.executemany(sql, v)
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()

Мне нравится такой подход. Он кажется простым и не волшебным, поддерживает все итерируемые типы и не требует импорта.

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

Довольно питонический здесь (вы также можете встроить тело функции split_groups)

import itertools
def split_groups(iter_in, group_size):
    return ((x for _, x in item) for _, item in itertools.groupby(enumerate(iter_in), key=lambda x: x[0] // group_size))

for x, y, z, w in split_groups(range(16), 4):
    foo += x * y + z * w

Это ответ разбивает список строк, f.ex. для достижения соответствия длине строки PEP8:

def split(what, target_length=79):
    '''splits list of strings into sublists, each 
    having string length at most 79'''
    out = [[]]
    while what:
        if len("', '".join(out[-1])) + len(what[0]) < target_length:
            out[-1].append(what.pop(0))
        else:
            if not out[-1]: # string longer than target_length
                out[-1] = [what.pop(0)]
            out.append([])
    return out

Использовать как

>>> split(['deferred_income', 'long_term_incentive', 'restricted_stock_deferred', 'shared_receipt_with_poi', 'loan_advances', 'from_messages', 'other', 'director_fees', 'bonus', 'total_stock_value', 'from_poi_to_this_person', 'from_this_person_to_poi', 'restricted_stock', 'salary', 'total_payments', 'exercised_stock_options'], 75)
[['deferred_income', 'long_term_incentive', 'restricted_stock_deferred'], ['shared_receipt_with_poi', 'loan_advances', 'from_messages', 'other'], ['director_fees', 'bonus', 'total_stock_value', 'from_poi_to_this_person'], ['from_this_person_to_poi', 'restricted_stock', 'salary', 'total_payments'], ['exercised_stock_options']]

Я никогда не хочу, чтобы мои куски были набиты, поэтому это требование очень важно. Я считаю, что способность работать с любыми итерациями также является требованием. Учитывая это, я решил продолжить принятый ответ, 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

Если вы не против использования внешнего пакета, вы можете использовать iteration_utilities.grouper из iteration_utilties1. Он поддерживает все итерации (а не только последовательности):

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. Далее следует подход Сумасшествие.

Тест был создан с помощью simple_benchmark1. Для запуска этого теста использовался следующий код:

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.

Я надеюсь, что, выключая итератор из списка, я не просто копирую часть списка. Генераторы могут быть нарезаны, и они автоматически останутся генераторами, в то время как списки будут нарезаны на огромные фрагменты по 1000 записей, что менее эффективно.

def iter_group(iterable, batch_size:int):
    length = len(iterable)
    start = batch_size*-1
    end = 0
    while(end < length):
        start += batch_size
        end += batch_size
        if type(iterable) == list:
            yield (iterable[i] for i in range(start,min(length-1,end)))
        else:
            yield iterable[start:end]

Применение:

items = list(range(1,1251))

for item_group in iter_group(items, 1000):
    for item in item_group:
        print(item)

Если я что-то не упустил, следующее простое решение с выражениями генератора не упоминалось. Предполагается, что известны как размер, так и количество блоков (что часто бывает) и что никаких отступов не требуется:

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

Почему бы не использовать понимание списка

l = [1 , 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
n = 4
filler = 0
fills = len(l) % n
chunks = ((l + [filler] * fills)[x * n:x * n + n] for x in range(int((len(l) + n - 1)/n)))
print(chunks)

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

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

В пакете more-itertools есть метод разбитый на части, который делает именно это:

import more_itertools
for s in more_itertools.chunked(range(9), 4):
    print(s)

Печать

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

chunked возвращает элементы в списке. Если вы предпочитаете итерации, используйте ichunked.

Другие вопросы по теме