Каково использование итера (вызываемого, дозорного)?

Итак, я смотрел, как Реймонд Хеттингер говорил Преобразование кода в красивый, идиоматический Python, и он поднимает эту форму iter, которую я никогда не осознавая. Его пример следующий:

Вместо:

blocks = []
while True:
    block = f.read(32)
    if block == '':
        break
    blocks.append(block)

Использование:

blocks = []
read_block = partial(f.read, 32)
for block in iter(read_block, ''):
    blocks.append(block)

После проверки документации iter, я нашел аналогичный пример:

with open('mydata.txt') as fp:
    for line in iter(fp.readline, ''):
        process_line(line)

Это выглядит очень полезно для меня, но мне было интересно, знаете ли вы, что Pythonistas знает какие-либо примеры этой конструкции, которые не связаны с циклами ввода-вывода? Возможно, в стандартной библиотеке?

Я могу придумать очень надуманные примеры, например следующее:

>>> def f():
...     f.count += 1
...     return f.count
... 
>>> f.count = 0
>>> list(iter(f,20))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> 

Но, очевидно, это не более полезно, чем встроенные итерации. Кроме того, мне кажется, что у меня запах кода, когда вы назначаете состояние функции. В этот момент я, скорее всего, буду работать с классом, но если я собираюсь написать класс, я мог бы также реализовать протокол итератора для чего бы я ни хотел.

Ответ 1

Как правило, основное использование, которое я видел для двух аргументов arg, включает в себя преобразование функций, которые аналогичны C API (неявное состояние, без понятия итерации) для итераторов. Файлоподобные объекты являются распространенным примером, но это проявляется в других библиотеках, которые плохо переносят C API. Предпочитаемый шаблон можно было бы увидеть в API, например, FindFirstFile/FindNextFile, где открывается ресурс, и каждый вызов переходит во внутреннее состояние и возвращает новое значение или маркерную переменную (например, NULL в C), Обертка его в классе, реализующем протокол итератора, как правило, лучше всего, но если вам нужно это делать самостоятельно, тогда как API является встроенным C-уровнем, обертка может в конечном итоге замедлить использование, где два arg iter, реализованные в C как ну, можно избежать расходов на выполнение дополнительного байтового кода.

Другие примеры включают изменяемые объекты, которые были изменены во время самого цикла, например, цикл в обратном порядке по строкам в байтовом режиме, удаление строки только после завершения обработки:

>>> from functools import partial
>>> ba = bytearray(b'aaaa\n'*5)
>>> for i in iter(partial(ba.rfind, b'\n'), -1):
...     print(i)
...     ba[i:] = b''
...
24
19
14
9
4

Другим случаем является использование разрезания прогрессивным способом, например, эффективный (если допускается, уродливый) способ группировать итерируемый в группы элементов n, позволяя конечной группе быть менее чем n элементов, если входной итерабельный не является четным числом n элементов в длине (этот я фактически использовал, хотя обычно я использую itertools.takewhile(bool вместо двух arg iter):

# from future_builtins import map  # Python 2 only
from itertools import starmap, islice, repeat

def grouper(n, iterable):
    '''Returns a generator yielding n sized tuples from iterable

    For iterables not evenly divisible by n, the final group will be undersized.
    '''
    # Keep islicing n items and converting to groups until we hit an empty slice
    return iter(map(tuple, starmap(islice, repeat((iter(iterable), n)))).__next__, ())  # Use .next instead of .__next__ on Py2

Другое использование: Написание нескольких маринованных объектов на один файл, за которым следует оценочное значение (None например), поэтому при распаковке вы можете использовать эту идиому вместо того, чтобы каким-то образом запомнить количество предметов маринованного или для вызова load снова и снова до EOFError:

with open('picklefile', 'rb') as f:
    for obj in iter(pickle.Unpickler(f).load, None):
        ... process an object ...

Ответ 2

Вот глупый пример, который я придумал:

from functools import partial
from random import randint

pull_trigger = partial(randint, 1, 6)

print('Starting a game of Russian Roulette...')
print('--------------------------------------')

for i in iter(pull_trigger, 6):
    print('I am still alive, selected', i)

print('Oops, game over, I am dead! :(')

Пример вывода:

$ python3 roulette.py 
Starting a game of Russian Roulette...
--------------------------------------
I am still alive, selected 2
I am still alive, selected 4
I am still alive, selected 2
I am still alive, selected 5
Oops, game over, I am dead! :(

Идея состоит в том, чтобы иметь генератор, который дает случайные значения, и вы хотите, чтобы процесс был выбран, когда определенное значение было выбрано. Вы можете, например, используйте этот шаблон в каждом прогоне моделирования, который пытается определить средний результат случайного процесса.

Конечно, процесс, который вы планировали бы, скорее всего, будет намного сложнее под капотом, чем простой бросок кубика...

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

from foo_lib import guess_password

for msg in iter(guess_password, ''):
    print('Incorrect attempt, details:', msg)

# protection cracked, continue...

Ответ 3

В многопроцессорном/многопоточном коде вы (надеюсь) часто найдете эту конструкцию для опроса очереди или канала. В стандартной библиотеке вы также найдете это в multiprocessing.Pool:

@staticmethod
def _handle_tasks(taskqueue, put, outqueue, pool, cache):
    thread = threading.current_thread()

    for taskseq, set_length in iter(taskqueue.get, None):
        task = None
        try:
            # iterating taskseq cannot fail
            for task in taskseq:
        ...
    else:
        util.debug('task handler got sentinel')

Некоторое время назад я наткнулся на эту запись в блоге, в которой IMO действительно замечательно iter(callable, sentinel) преимущество iter(callable, sentinel) над while True... break:

Обычно, когда мы перебираем объекты или пока не произойдет условие, мы понимаем область действия цикла в его первой строке. Например, когда мы читаем цикл, который начинается с книги в книгах, мы понимаем, что перебираем все книги. Когда мы видим цикл, который начинается с while, а не battery.empty(), мы понимаем, что область действия цикла - пока у нас есть батарея. Когда мы говорим "Делай навсегда" (то есть, пока Истина), становится очевидным, что эта сфера - ложь. Таким образом, это требует, чтобы мы держали эту мысль в своей голове и искали в остальной части кода утверждение, которое вытащит нас из этого. Мы входим в цикл с меньшим количеством информации, и поэтому он менее читабелен.