Генераторы в Python
Введение
В этой статье вы узнаете о том что такое генераторы в Python 3.
Перед тем как разбираться с генераторами нужно хорошо представлять себе
что такое
итераторы
.
Советую убедиться в том, что вы знакомы с материалом из статьи
«Итерация, итерируемые объекты и итераторы в Python»
Для чего нужны генераторы
По объектам
списка
или другого
итерируемого объекта
можно перемещаться разными способами. Если список
небольшой - прекрасно подойдёт, например, функция
enumerate
Генераторы нужны для больших объектов. Например, для таких, которые не помещаются
в памяти.
Так как их невозможно загрузить целиком, обычные способы итерации не подходят и на
помощь приходят генераторы.
Идея состоит в том, что особым образом передаётся только следующий
элемент объекта - это называется lazy evaluation или ленивое вычисление
Ключевое слово, по которому можно опознать генератор в коде это
yield
Генератор может включать в себя и обычные return но чтобы быть
генератором нужен хотя бы один yield
Хочу отметить, что создать генератор можно не написав ни одного yield
самостоятельно, например
так
.
Создайте файл
generators_demo.py
и копируйте туда код из примеров.
Запустить файл можно командой
python generators_demo.py
def gen123(): yield 1 yield 2 yield 3 g = gen123() print(g) print(next(g)) print(next(g)) print(next(g)) print(next(g)) # нужно закомментировать чтобы код работал дальше
python generators_demo.py
<generator object gen123 at 0x01AADCD8> 1 2 3 Traceback (most recent call last): File "generators_demo.py", line 12, in <module> print(next(g)) StopIteration
Каждый вызов функции-генератора создаёт новый генератор-объект (generator object)
h = gen123()
i = gen123()
print(h)
print(i)
print(h is i)
<generator object gen123 at 0x00F7DD48>
<generator object gen123 at 0x00F7DD80>
False
Соответственно и итерация по ним независимая
print(next(h))
print(next(h))
print(next(i))
1
2
1
def gen246():
print("About to yield 2")
yield 2
print("About to yield 4")
yield 4
print("About to yield 6")
yield 6
print("About to return")
g = gen246()
print(next(g))
print(next(g))
print(next(g))
print(next(g))
python generators_demo.py
About to yield 2 2 About to yield 4 4 About to yield 6 6 About to return Traceback (most recent call last): File "generators_demo.py", line 43, in <module> print(next(g)) StopIteration
Где вы уже сталкивались с генератором
Если вы создавали цикл for как в примере ниже то уже использовали генератор. Этот синтаксис похожи на list comprehension только скобки как у кортежа а не фигурные (tuple comprehension в Python не существует)
my_dict = {"a": 1, "b": 2, "c": 3, "d":4} letters = (l for l in my_dict) print(type(letters))
python generators_example.py
<class 'generator'>
Ещё один пример генератора
>>> my_set = {1, 2, 3} >>> squares = (n**2 for n in my_set) >>> squares <generator object <genexpr> at 0x00000159BD7BF780> >>> next(squares) 1 >>> next(squares) 4 >>> next(squares) 9 >>> next(squares) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Maintaining State в генераторах
Рассмотрим код, который будет возвращать из списка определённое количество неповторяющихся элементов
def take(count, iterable): counter = 0 for item in iterable: if counter == count: return counter += 1 yield item def distinct(iterable): seen = set() for item in iterable: if item in seen: continue yield item seen.add(item) # continue - finish current loop iteration and begin the next iteration immediately def run_pipeline(): items = [3, 6, 6, 2, 1, 1] for item in take(3, distinct(items)): print(item) run_pipeline()
distinct() - это генератор, который выдаёт по одному элементу, если
этого элемента нет во множестве (в set) seen
take() - это тоже генератор - он просто берет определённое количество элементов
python generators_demo.py
3 6 2
Каждый вызов функции-генератора создаёт новый генератор-объект (generator object)
Чтобы разобраться в работе этого примера можно использовать debugger, например
PyCharm
В качестве дополнительной иллюстрации, которая особенно пригодится чуть позже
используем обычный print().
Добавим ещё пару элементов в items
def take(count, iterable): counter = 0 for item in iterable: print("take item", item) if counter == count: print("No need to take it, return now") return counter += 1 yield item def distinct(iterable): seen = set() for item in iterable: print("distinct item candidate", item) if item in seen: continue else: print("distinct item", item) yield item seen.add(item) # continue - finish current loop iteration and begin the next iteration immediately def run_pipeline(): items = [3, 6, 6, 2, 1, 1, 5, 5] for item in take(3, distinct(items)): print("run_pipeline item", item) print(item) run_pipeline()
distinct item candidate 3 distinct item 3 take item 3 run_pipeline item 3 3 distinct item candidate 6 distinct item 6 take item 6 run_pipeline item 6 6 distinct item candidate 6 distinct item candidate 2 distinct item 2 take item 2 run_pipeline item 2 2 distinct item candidate 1 distinct item 1 take item 1 No need to take it, return now Process finished with exit code 0
Как видите, сперва работает take(), затем distinct(), затем run_pipeline()
take() когда берёт 1 понимает, что она уже лишняя (нужно всего 3 элемента) и до 5 дело вообще не доходит.
Произведено ровно столько работы, сколько необходимо, это так называемое ленивое вычисление (lazy computing)
Если становится слишком сложно, можно уйти от этой схемы, заставив distinct() выполнить все вычисления
прежде чем они попадут в take().
Для этого вызовем disctinct() из list()
def run_pipeline(): items = [3, 6, 6, 2, 1, 1] for item in take(3, list(distinct(items))): print("run_pipeline item", item)
distinct item candidate 3 distinct item 3 distinct item candidate 6 distinct item 6 distinct item candidate 6 distinct item candidate 2 distinct item 2 distinct item candidate 1 distinct item 1 distinct item candidate 1 distinct item candidate 5 distinct item 5 distinct item candidate 5 take item 3 run_pipeline item 3 take item 6 run_pipeline item 6 take item 2 run_pipeline item 2 take item 1 No need to take it, return now Process finished with exit code 0
В этом случае проделана лишняя работы - distinct() прошёлся по всем элементам и в результате
получился
список
[3, 6, 2, 1, 5]
из которого take() взял нужные три элемента.
В данном примере разница невелика, но если бы в items было не 8 а миллион элементов, мы бы её почувстовали
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы. Если там пусто считайте это рекламой моей телеги
Производительность
Сравним скорость выполнения этих скриптов. Сделаем это с помощью декоратора prof_timer , который нужно поместить выше вызова run_pipeline()
def prof_timer(orig_func): import time def wrapper(*args, **kwargs): t1 = time.time() result = orig_func(*args, **kwargs) t2 = time.time() - t1 print(f'{orig_func.__name__} ran in: {t2} sec') return result return wrapper
@prof_timer def run_pipeline(): items = [3, 6, 6, 2, 1, 1, 5, 5]
Lazy:
run_pipeline ran in: 6.0249837585029e-05 sec
С list():
run_pipeline ran in: 6.4849853515625e-05 sec
Разница незаметна. Добавим элементов в items
@prof_timer def run_pipeline(): items = [ 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5, 3, 6, 6, 2, 1, 1, 5, 5,]
Lazy:
run_pipeline ran in: 0.00011014938354492188 sec
С list():
run_pipeline ran in: 0.0005578994750976562 sec
Lazy быстрее в 5 раз
Генератор чисел Фибоначчи
Генератор, который выдаёт числа Фибоначчи.
Для примера запустим вариант, который покажет первые 20 чисел Фибоначчи.
Чтобы запустить бесконечную генерацию - раскомментируйте нижний блок кода
def fib_gen(): yield 0 a = 1 b = 1 while True: yield b a, b = b, a+b g = fib_gen() for i in range(20): print(next(g)) # Uncomment to generate infinit number # of Fibonacci numbers # import time # time.sleep(3) # for f in fib_gen(): # print("\nf: ",f)
python fibon.py
0 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
Generator Expressions
Синтаксис
(expr(item) for item in iterable)
Чтобы передать генератор в функцию как аргумент дополнительные скобки не обязательны
func(expr(item) for item in iterable)
Рассмотрим пример
million_squares = (x*x for x in range(1, 1000001)) print(million_squares) print(list(million_squares)[-10:]) print(list(million_squares)[-10:]) # [] # To recreate a generator from a # generator expression, you must # execute the expression again
python gen_expr.py
<generator object <genexpr> at 0x7fdc7ada4580> [999982000081, 999984000064, 999986000049, 999988000036, 999990000025, 999992000016, 999994000009, 999996000004, 999998000001, 1000000000000] []
Второй вызов list() ничего не дал, так как генератор уже отработал до конца.
Вычислим сумму чисел передав генератор в sum()
print(sum(x*x for x in range(1, 1000001)))
333333833333500000
Вычислим сумму всех простых чисел меньших 1000.
Если мы захотим вычислить суммы первых тысячи простых чисел, то подход из следующего
пример не подойдёт - так как мы не знаем какой точно ставить range. Как обойти эту
проблему с помощью
itertools
читайте
здесь
def is_prime(x): from math import sqrt if x < 2: return False for i in range(2, int(sqrt(x)) + 1): if x % i == 0: return False return True print(sum(x for x in range(1001) if is_prime(x)))
76127
itertools
В библиотеку itertools входят функции, которые можно использовать для создания итераторов.
count() - бесконечный численный итератор
from itertools import count # Start from 0 step 1 a = count() print(type(a)) for i in range(10): print(next(a)) # Start from 100 step 50 c = count(100, 50) for i in range(5): print(next(c))
<class 'itertools.count'> 0 1 2 3 4 5 6 7 8 9 100 150 200 250 300
В itertools есть функция islice(), которую удобно использовать вместо for из предыдущего примера
print(list(islice((x for x in count(100, 50)), 20)))
[100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050]
Получить список из 1000 простых чисел, начиная с 1
from itertools import count, islice def is_prime(x): from math import sqrt if x < 2: return False for i in range(2, int(sqrt(x)) + 1): if x % i == 0: return False return True thousand_primes = islice((x for x in count() if is_prime(x)), 1000) print(thousand_primes) print(list(thousand_primes)[-10:])
python iter_tools.py
<itertools.islice object at 0x7f960f4109f0> [7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919]
С помощью этого же метода можно вычислить сумму первых тысячи простых чисел
from itertools import count, islice def is_prime(x): from math import sqrt if x < 2: return False for i in range(2, int(sqrt(x)) + 1): if x % i == 0: return False return True thousand_primes = islice((x for x in count() if is_prime(x)), 1000) print("Sum of first 1000 primes: ", sum(thousand_primes))
Sum of first 1000 primes: 3682913
Пример применения itertools chain вы можете изучить здесь
all(), any(), zip()
Разберем применение функций all() , any() , zip() к генераторам
Напомню принцип работы all() и any()
# any() print(any([False, False, False])) # -> False print(any([False, False, True])) # -> True # all() print(all([True, True, True])) # -> True print(all([True, False, True])) # -> False
Пременим эти функции к генераторам.
print(any(is_prime(x) for x in range(1328, 1361))) print(any(is_prime(x) for x in range(99, 102))) print(all(is_prime(x) for x in range(99, 102))) print(all(name == name.title() for name in ['Benalmadena', 'Cordoba', 'Fuengirola', 'Malaga']))
False True False True
Убедимся, что zip() выдаёт (yields) кортежи
# temparature sunday = [12, 14, 15, 15, 17, 21, 22, 22, 23, 22, 20, 18] monday = [13, 14, 14, 14, 16, 20, 21, 22, 22, 21, 19, 17] for item in zip(sunday, monday): print(item) ~
python zip_ex.py
(12, 13) (14, 14) (15, 14) (15, 14) (17, 16) (21, 20) (22, 21) (22, 22) (23, 22) (22, 21) (20, 19) (18, 17)
У объекта класса zip есть методы __iter__ и __next__
z = zip(sunday, monday) print(type(z)) print(dir(z)) print(next(z)) print(next(z))
<class 'zip'> ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] (12, 13) (14, 14)
for sun, mon in zip(sunday, monday): print("average =", (sun + mon) / 2)
average = 12.5 average = 14.0 average = 14.5 average = 14.5 average = 16.5 average = 20.5 average = 21.5 average = 22.0 average = 22.5 average = 21.5 average = 19.5 average = 17.5
Добавим ещё один день недели
tuesday = [2, 2, 3, 7, 9, 10, 11, 12, 10, 9, 8, 8] for temps in zip(sunday, monday, tuesday): print( f"min = {min(temps):4.1f}, max={max(temps):4.1f}," f"average={sum(temps) / len(temps):4.1f}")
min = 2.0, max=13.0,average= 9.0 min = 2.0, max=14.0,average=10.0 min = 3.0, max=15.0,average=10.7 min = 7.0, max=15.0,average=12.0 min = 9.0, max=17.0,average=14.0 min = 10.0, max=21.0,average=17.0 min = 11.0, max=22.0,average=18.0 min = 12.0, max=22.0,average=18.7 min = 10.0, max=23.0,average=18.3 min = 9.0, max=22.0,average=17.3 min = 8.0, max=20.0,average=15.7 min = 8.0, max=18.0,average=14.3
С помощью itertools chain можно перебрать элементы всех трёх списков без конкатенации
from itertools import chain temperatures = chain(sunday, monday, tuesday) print(all(t > 0 for t in temperatures))
True
Только простые числа Фибоначчи
Рассмотрим скрипт fibon_prime.py который будет генерировать только простые числа Фибоначчи
def is_prime(x): from math import sqrt if x < 2: return False for i in range(2, int(sqrt(x)) + 1): if x % i == 0: return False return True def fib_gen(): yield 0 a = 1 b = 1 while True: yield b a, b = b, a+b for x in (f for f in fib_gen() if is_prime(f)): print(x)
python fibon_prime.py
2 3 5 13 89 233 1597 28657 514229 433494437 2971215073 99194853094755497
StopIteration
Пройдёмся по всему итератору и обработаем исключение StopIteration
cities = ['Benalmadena', 'Cordoba', 'Fuengirola', 'Malaga'] g = iter(cities) while True: try: print(next(g)) except StopIteration: raise ValueError("iterable is empty") finally: print("Printing!") print("Great Success!") # will not be printed
python iterable_ex2.py
Benalmadena Printing! Cordoba Printing! Fuengirola Printing! Malaga Printing! Printing! Traceback (most recent call last): File "/home/andrei/iterable_ex2.py", line 21, in <module> print(next(g)) StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/andrei/iterable_ex2.py", line 23, in <module> raise ValueError("iterable is empty") ValueError: iterable is empty
Резюме
Генератор - это объект, который создаётся с помощью yield.
У него есть метод __next__
Чтобы пользоваться этим объектом, нужно либо вручную вызывать next() или .__next__
либо использовать функцию, которая умеет это делать, например: list(), sum() и т.д.
главное, чтобы она могла пробегать по итерируемым объектам.
Итерируемый объект (iterable) это такой объект, из которого можно сделать
итератор (iterator), применив к нему функцию iter().
Итератор (iterator) это объект, который можно передать в функцию next() чтобы
получить следующий элемент последовательности (sequence)
Generator Comprehensions
# Multi-input Generator Comprehension g = ((x, y) for x in range(5) for y in range(x)) print(type(g)) print(list(g))
<class 'generator'> [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2), (4, 3)]
Итерация | |
Функции | |
Лямбда функции | |
all() | |
map() | |
Python | |
if, elif, else | |
Циклы | |
Методы | |
Генераторы списков | |
Генераторы словарей | |
Генераторы множеств | |
*args **kwargs | |
enum |
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы. Если там пусто считайте это рекламой моей телеги