Мастерство стандартной библиотеки: бесконечные итераторы itertools
Продолжаем наше исследование модуля 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!
Надеюсь, вам понравилось. Может, подпишитесь? Мы планируем продолжать. У нас есть телеграм канал - там будут оповещения по всем новым материалам. И еще у нас есть DEV - там будут переводы на английский, и там можно комментировать. И читать комменты.
А есть что почитать дополнительно?
Конечно.