Продолжаем наше исследование модуля itertools.

На очереди 3 конструктора бесконечных итераторов:

from itertools import count, cycle, repeat

itertools.count

itertools.count - это как range, только он бесконечный и ленивый.

Кстати, если вы раньше не слышали термин ленивость (хотя я почему-то уверен, что мы все его слышали и даже практиковали) - то бегом читать хотя бы сюда. Когда-нибудь мы пройдем путями Дэвида Бизли и его легендарного 147 страничного манускрипта “Generator Tricks For Systems Programmers”, но не сегодня. Сегодня - об относительно простых вещах.

Вообще, count - это супер просто, он всего лишь считает до бесконечности. Ну или минус бесконечности, если step отрицательный.

def my_count(start=0, step=1):
    x = start
    while True:
        yield x
        x += step

И все.

Но есть нюанс. У него нет конца, то есть его нельзя сконсюмить.

Сконсюмить - это разом прочитать весь iterable, например для складирования в list.

Ну то есть можно, но вот эта питонья строчка гарантированно положит любую рабочую станцию. И да, быстро-быстро понажимать Ctrl+C не помогает) Только hard reset, я вас предупредил.

list(itertools.count())

Как же с ним тогда работать, если его нельзя привести к list/set, на него нельзя “скастовать” sum и т.д.

Ну, во-первых по нему можно бежать (и вовремя из него выйти):

for i in count(start=10, step=-1):
    print(i, end=", ")
    if i<=0: break

# 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,

И его можно комбинировать с другими ленивыми итераторами, например map, zip, islice, accumulate и т.д.

Известно, что итераторы вроде map и zip при пробежке по нескольким iterable одновременно, заканчиваются когда хотя бы один из iterable заканчивается. Это дает нам exit из бесконечного итератора.

Пример взят из доков itertools.repeat:

list(map(pow, range(10), repeat(2)))
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Мы не положили сервак, хотя вроде бы пытаемся сконсюмить через list бесконечный repeat. К счастью (или к печали), range конечен и map закончится вместе с ним.

Бесконечный итератор отказывается от своей бесконечности, чтобы закончиться вместе с конечной коллекцией. Прям какой-то вайб из старого фильма Горец и песни Queen - Who Wants To Live Forever

itertools.repeat

itertools.repeat еще проще, чем itertools.count. Он даже не считает, а просто повторяет одно и тоже либо указанное количество раз, либо бесконечно.

В доках itertools приводится такое:

def repeat(object, times=None):
    # repeat(10, 3) --> 10 10 10
    if times is None:
        while True:
            yield object
    else:
        for i in range(times):
            yield object

Мы приведем вот такой эквивалент для фиксированного количество повторений:

( 42 for _ in range(10) )   # lazy, это итератор repeat(42, 10)
[ 42 for _ in range(10) ]   # не lazy, это лист list(repeat(42, 10))

Легко также заметить, что itertools.count при step=0 - это itertools.repeat

Наверное, repeat и count добавляют немного читаемости в код, и есть подозрение что они могут быть быстрее, но проверить итераторы на скорость не так просто, ведь они одноразовые, а тест скорости - это многократное повторение и сравнение.

Но давайте попробуем так:

In [49]: i1 = lambda: ( 42 for _ in range(100000) )

In [50]: i2 = lambda: repeat(42, 100000)

In [51]: %timeit sum(i1())
3.49 ms ± 36.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [52]: %timeit sum(i2())
333 µs ± 1.27 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

itertools.repeat оказался на порядок быстрее (один порядок - это в 10 раз в десятичной системе, если кто не знал)

К слову, покритикуйте мой тест на performance: как думаете, хороша ли фабрика с помощью lambda и валидные ли получились цифры и выводы?

itertools.cycle

Бесконечный повтор. Супер просто:

# cycle('ABCD') --> A B C D A B C D ...

def my_cycle(iterable):
    while True:
        yield from iterable

Несмотря на простоту, эта штука очень удобна.

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

Например, можно объявить “глобальные” итераторы:

PROXY_CYCLE = itertools.cycle(proxy_list)
UA_CYCLE = itertools.cycle(ua_list)

И каждый раз когда вам нужно сделать новый запрос, вы просто просите у “глобальных” итераторов новые значения через next:

proxy = next(PROXY_CYCLE)
ua = next(UA_CYCLE)

Получается такое распределенное итерирование из разных мест программы одновременно. Но при этом итератор - один на всех. Итератор как сервис, хех.

Как будто мы написали класс ProxyManager и объявили у него ProxyManager.get, который определяет выдачу нам новых проксей. Только вместо class у нас itertools.cycle, а вместо get - у нас next. Так нужно ли объявлять класс? :)

That’s all, folks!

Надеюсь, вам понравилось.

А есть что почитать дополнительно?

Конечно.

Гайд по функциональному программированию на python

Для смелых

Разумеется, доки itertools