Разделите генератор на куски без предварительной ходьбы

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

Я хотел бы разделить генератор на куски. Требования:

  • не вставляйте куски: если количество оставшихся элементов меньше размера куска, последний кусок должен быть меньше.
  • не прогоните генератор заранее: вычисление элементов является дорогостоящим, и это должно выполняться только функцией потребления, а не цукнером
  • что означает, конечно: не накапливать в памяти (без списков)

Я пробовал следующий код:

def head(iterable, max=10): for cnt, el in enumerate(iterable): yield el if cnt >= max: break def chunks(iterable, size=10): i = iter(iterable) while True: yield head(i, size) # Sample generator: the real data is much more complex, and expensive to compute els = xrange(7) for n, chunk in enumerate(chunks(els, 3)): for el in chunk: print 'Chunk %3d, value %d' % (n, el) 

И это как-то работает:

 Chunk 0, value 0 Chunk 0, value 1 Chunk 0, value 2 Chunk 1, value 3 Chunk 1, value 4 Chunk 1, value 5 Chunk 2, value 6 ^CTraceback (most recent call last): File "xxxx.py", line 15, in <module> for el in chunk: File "xxxx.py", line 2, in head for cnt, el in enumerate(iterable): KeyboardInterrupt 

Буууут … он никогда не останавливается (я должен нажать ^C ) из-за while True . Я хотел бы остановить этот цикл всякий раз, когда генератор был израсходован, но я не знаю, как обнаружить эту ситуацию. Я попытался сделать исключение:

 class NoMoreData(Exception): pass def head(iterable, max=10): for cnt, el in enumerate(iterable): yield el if cnt >= max: break if cnt == 0 : raise NoMoreData() def chunks(iterable, size=10): i = iter(iterable) while True: try: yield head(i, size) except NoMoreData: break # Sample generator: the real data is much more complex, and expensive to compute els = xrange(7) for n, chunk in enumerate(chunks(els, 2)): for el in chunk: print 'Chunk %3d, value %d' % (n, el) 

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

 Chunk 0, value 0 Chunk 0, value 1 Chunk 0, value 2 Chunk 1, value 3 Chunk 1, value 4 Chunk 1, value 5 Chunk 2, value 6 Traceback (most recent call last): File "xxxx.py", line 22, in <module> for el in chunk: File "xxxx.py", line 9, in head if cnt == 0 : raise NoMoreData __main__.NoMoreData() 

Как я могу обнаружить, что генератор исчерпан в функции chunks , не прогуливаясь?

  • Есть ли способ выполнить цикл и выполнить все функции в классе Python?
  • Как избежать круговых зависимостей при настройке свойств?
  • Взгляд флажка вызывает TypeError: объект «bool» не может быть вызван
  • Объект вызова Introspect
  • Динамически привязать метод к экземпляру класса в python
  • КАК использовать использование ткани с dtach, экраном, есть ли какой-то пример
  • python: Помогите реализовать алгоритм для поиска прямоугольника минимальной площади для заданных точек, чтобы вычислить основную и малую длину оси
  • Можете ли вы сделать счетчик без выписки «Счетчик»?
  • 9 Solutions collect form web for “Разделите генератор на куски без предварительной ходьбы”

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

     def head(iterable, max=10): first = next(iterable) # raise exception when depleted def head_inner(): yield first # yield the extracted first element for cnt, el in enumerate(iterable): yield el if cnt + 1 >= max: # cnt + 1 to include first break return head_inner() 

    Просто используйте это в своем генераторе chunk и поймайте исключение StopIteration как это было сделано с вашим специальным исключением.


    Обновление: вот еще одна версия, использующая itertools.islice для замены большей части функции head и цикла for . Этот простой цикл в действительности делает то же самое, что и эта громоздкая конструкция while-try-next-except-break в исходном коде, поэтому результат гораздо читабельнее.

     def chunks(iterable, size=10): iterator = iter(iterable) for first in iterator: # stops when iterator is depleted def chunk(): # construct generator for next chunk yield first # yield element from for loop for more in islice(iterator, size - 1): yield more # yield more elements from the iterator yield chunk() # in outer generator, yield next chunk 

    И мы можем стать еще короче, используя itertools.chain для замены внутреннего генератора:

     def chunks(iterable, size=10): iterator = iter(iterable) for first in iterator: yield chain([first], islice(iterator, size - 1)) 

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

    Каждая итерация groupby вызывает next метод объекта count и генерирует ключ группы / chunk (за которым следуют элементы в куске) путем выполнения целочисленного деления текущего значения счета на размер куска.

     from itertools import groupby, count def chunks(iterable, size=10): c = count() for _, g in groupby(iterable, lambda _: next(c)//size): yield g 

    Каждая группа / кусок g заданная функцией генератора, является итератором.

    Самое быстрое решение, с которым я смог придумать, благодаря (в CPython), используя чисто встроенные C-уровни. Поступая таким образом, байт-код Python не требуется для создания каждого фрагмента (если базовый генератор не реализован на Python), который имеет огромное преимущество в производительности. Он выполняет каждый кусок перед его возвратом, но он не делает никаких предварительных шагов за кусок, который он собирается вернуть:

     # Py2 only to get generator based map from future_builtins import map from itertools import islice, repeat, starmap, takewhile def chunker(n, iterable): # n is size of each chunk; last chunk may be smaller return takewhile(bool, map(tuple, starmap(islice, repeat((iter(iterable), n))))) 

    Так как это немного плотно, развернутая версия для иллюстрации:

     def chunker(n, iterable): iterable = iter(iterable) while True: x = tuple(islice(iterable, n)) if not x: return yield x 

    chunker вызова в chunker в enumerate позволит вам chunker количество кусков, если это необходимо.

    Как насчет использования itertools.islice :

     import itertools els = iter(xrange(7)) print list(itertools.islice(els, 2)) print list(itertools.islice(els, 2)) print list(itertools.islice(els, 2)) print list(itertools.islice(els, 2)) 

    Который дает:

     [0, 1] [2, 3] [4, 5] [6] 
     from itertools import islice def chunk(it, n): ''' # returns chunks of n elements each >>> list(chunk(range(10), 3)) [ [0, 1, 2, ], [3, 4, 5, ], [6, 7, 8, ], [9, ] ] >>> list(chunk(list(range(10)), 3)) [ [0, 1, 2, ], [3, 4, 5, ], [6, 7, 8, ], [9, ] ] ''' def _w(g): return lambda: tuple(islice(g, n)) return iter(_w(iter(it)), ()) 

    Вы сказали, что не хотите хранить вещи в памяти, значит ли это, что вы не можете создать промежуточный список для текущего фрагмента?

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

     class Sentinel(object): pass def chunk(els, size): for i, el in enumerate(els): yield el if i > 0 and i % size == 0: yield Sentinel 

    ИЗМЕНИТЬ другое решение с генератором генераторов

    Вы не должны делать некоторое while True в своем итераторе, а просто прокручиваете его и обновляете номер фрагмента на каждой итерации:

     def chunk(it, maxv): n = 0 for i in it: yield n // mavx, i n += 1 

    Если вы хотите генератор генераторов, вы можете:

     def chunk(a, maxv): def inner(it, maxv, l): l[0] = False for i in range(maxv): yield next(it) l[0] = True raise StopIteration it = iter(a) l = [True] while l[0] == True: yield inner(it, maxv, l) raise StopIteration 

    с существованием итерабельным.

    Тесты: на python 2.7 и 3.4:

     for i in chunk(range(7), 3): print 'CHUNK' for a in i: print a 

    дает:

     CHUNK 0 1 2 CHUNK 3 4 5 CHUNK 6 

    И на 2.7:

     for i in chunk(xrange(7), 3): print 'CHUNK' for a in i: print a 

    дает тот же результат.

    Но BEWARE : list(chunk(range(7)) блокирует на 2.7 и 3.4

    У меня была эта же проблема, но было найдено более простое решение, чем упомянутые здесь:

     def chunker(iterable, chunk_size): els = iter(iterable) while True: next_el = next(els) yield chain([next_el], islice(els, chunk_size - 1)) for i, chunk in enumerate(chunker(range(11), 2)): for el in chunk: print(i, el) # Prints the following: 0 0 0 1 1 2 1 3 2 4 2 5 3 6 3 7 4 8 4 9 5 10 

    Начал осознавать полезность этого сценария при разработке решения для вставки БД 500k + строк на более высокой скорости.

    Генератор обрабатывает данные из источника и «выводит» его по строкам; а затем другой генератор группирует вывод в куски и «выдает» его кусок куском. Второй генератор знает только размер куска и ничего больше.

    Ниже приведен образец, чтобы подчеркнуть концепцию:

     #!/usr/bin/python def firstn_gen(n): num = 0 while num < n: yield num num += 1 def chunk_gen(some_gen, chunk_size=7): res_chunk = [] for count, item in enumerate(some_gen, 1): res_chunk.append(item) if count % chunk_size == 0: yield res_chunk res_chunk[:] = [] else: yield res_chunk if __name__ == '__main__': for a_chunk in chunk_gen(firstn_gen(33)): print(a_chunk) 

    Протестировано в Python 2.7.12:

     [0, 1, 2, 3, 4, 5, 6] [7, 8, 9, 10, 11, 12, 13] [14, 15, 16, 17, 18, 19, 20] [21, 22, 23, 24, 25, 26, 27] [28, 29, 30, 31, 32] 
    Python - лучший язык программирования в мире.