Генераторы в Python

Содержание
Введение
Для чего нужны генераторы
Где вы уже сталкивались с генератором
Maintaining State в генераторах
Производительность
Генератор чисел Фибоначчи
Generator Expressions
itertools
all(), any(), zip()
Только простые числа Фибоначчи
StopIteration
Резюме
Похожие статьи

Введение

В этой статье вы узнаете о том что такое генераторы в 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

РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе

Конец рекламы. Если там пусто считайте это рекламой моей телеги

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: