Подтвердить что ты не робот

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

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

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

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

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

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

Buuuut... он никогда не останавливается (я должен нажать ^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, не пройдя его?

4b9b3361

Ответ 1

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

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, Этот простой цикл 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))

Ответ 2

Другой способ создания групп/фрагментов, а не предварительный переход генератора, использует 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, предоставляемая функцией генератора, является итератором. Однако, поскольку groupby использует общий итератор для всех групп, итераторы групп не могут быть сохранены в списке или в любом контейнере, каждый итератор каждой группы должен быть использован до следующего.

Ответ 3

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

# Py2 only to get generator based map
from future_builtins import map

from itertools import islice, repeat, starmap, takewhile
# operator.truth is *significantly* faster than bool for the case of
# exactly one positional argument
from operator import truth

def chunker(n, iterable):  # n is size of each chunk; last chunk may be smaller
    return takewhile(truth, 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 количество блоков, если это необходимо.

Ответ 4

Как насчет использования 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]

Ответ 5

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

Ответ 6

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

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

Ответ 7

Начал осознавать полезность этого сценария при разработке решения по вставке БД 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]

Ответ 8

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

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

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

Ответ 9

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

Вы не должны делать 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