BugBug

Ключевое слова yield и генераторы в Python

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

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

Отличия генераторов и списков.

Давайте на практике разберемся в отличии списков от генераторов. Для начала создадим список:


>>> simple_list=[x**2 for x in range(5)]
>>> type(simple_list)

>>> for i in simple_list:
	print(i)
0
1
4
9
16
						
Как и ожидается тип переменной simple_list является список и перебор элементов выводит ожидаемый результат.

Давайте теперь выполним ту же операцию, но теперь используем генератор:


>>> simple_generator=(x**2 for x in range(5))
>>> type(simple_generator)

>>> for i in simple_generator:
	print(i)
0
1
4
9
16
						
Обратите внимание на то, что при создании генератора используются круглые скобки вместо квадратных. Переменная simple generator имеет уже другой тип, а именно generator. Как вы видите перебор элементов в цикле дает такой же результат как и в предыдущем примере и тут у вас может возникнуть вопрос: так в чем же отличия генератора от обычного списка? Одно из главных их отличий в способе хранения элементов в памяти. Списки хранят сразу все элементы в памяти, в то время как генераторы создают свои элементы на лету в процессе итерации, отображая элемент и удаляя его при переходе к следующему. Это приводит к тому, что для повторного использования генератора его необходимо инициализировать снова.

Использование ключевого слова yield.

После того как мы узнали разницу в работе генераторов и простых коллекций давайте разберем как создавать генераторы с использованием yield.

В предыдущем примере мы создали генератор неявно. Однако в более сложных случаях мы можем создавать функцию возвращающую генератор. Для того что бы обычную функцию превратить в генератор, в теле функции используется ключевое слово yield вместо return. Давайте рассмотрим на примере как это работает:


>>> def square_num(nums):
	for i in nums:
		yield(i**2)
>>> sqr=square([1,2,3,4])
>>> print(sqr)

						
В примере функция square_num использует ключевое слово yield для возврата значения квадрата числа внутри цикла for. Несмотря на то, что мы вызываем функцию square_num, она на самом деле не выполняется на данный момент времени, и в памяти еще нет вычисленных значений.

Для того что бы запустить функцию-генератор на исполнения и получить результат её работы необходимо использовать встроенный метод генератора next. Данный метод возвращает первый элемент генератора, а при последующем использовании переходит к следующему элементу. Используемый метод будет возвращать значения пока не будет достигнут последнее значение в генераторе. При достижении конца генератора метод next возвращает ошибку StopIteration:


>>> next(sqr)
1
>>> next(sqr)
4
>>> next(sqr)
9
>>> next(sqr)
16
>>> next(sqr)
Traceback (most recent call last):
  File "", line 1, in 
    next(sqr)
StopIteration
						

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

Для получения данных из функции-генератора вместо постоянного использования метода next можно использовать цикл for для итерации по её значениям.

Оптимизация производительности.

Как уже говорилось ранее, генераторы удобны, когда речь заходит о задачах в которых обрабатывается большие объемы данных. Поскольку элементы в генераторе создаются в момент вызова и удаляются сразу после использования, объем требуемой памяти значительно сокращается.

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

С начала рассмотрим вариант без использования генератора. Создадим функцию которая возвращает список содержащий 1000000 элементов и вычислим используемую память до и после ее вызова. Для вычисления используемой памяти необходимо установить пакет psutil используемый для получения информации о запущенных процессах и использовании системы, это можно сделать воспользовавшись командой pip install psutil.


import time
import random
import os
import psutil
colors=['White','Black','Yellow','Red']
cars=['Nissan','Toyota','Honda','Audi']

def list_of_car(quantity):
    all_cars=[]
    for i in range(quantity):
        car={
            'color':random.choice(colors),
            'name':random.choice(cars),
            'id':i
        }
        all_cars.append(car)
    return all_cars

proc=psutil.Process(os.getpid())
print("Используемая память до выполнения функции: "+str(proc.memory_info().rss/1000000))
start=time.clock()
car=list_of_car(1000000)
stop=time.clock()

proc=psutil.Process(os.getpid())
print("Используемая память после выполнения функции: "+str(proc.memory_info().rss/1000000))
print("Заняло {} секунд".format(stop-start))
					

В результате выполнения данного кода я получил следующий результат(ваш может выглядеть иначе):


Используемая память до выполнения функции: 13.68064
Используемая память после выполнения функции: 196.104192
Заняло 4.18415977319766 секунд
						

До вызова функции использовалось 13 MB памяти, а после создания списка из 1000000 элементов занимаемая память выросла до 196 MB. При этом время необходимое на выполнение операции составило 13,6 секунды.

Теперь давайте используем функцию-генератор. Разница в коде совсем небольшая, как вы можете заметить ниже:


import time
import random
import os
import psutil
colors=['White','Black','Yellow','Red']
cars=['Nissan','Toyota','Honda','Audi']

def list_of_car(quantity):
    
    for i in range(quantity):
        car={
            'color':random.choice(colors),
            'name':random.choice(cars),
            'id':i
        }
        yield car

proc=psutil.Process(os.getpid())
print("Используемая память до выполнения функции: "+str(proc.memory_info().rss/1000000))
start=time.clock()
car=list_of_car(1000000)
stop=time.clock()

proc=psutil.Process(os.getpid())
print("Используемая память после выполнения функции: "+str(proc.memory_info().rss/1000000))
print("Заняло {} секунд".format(stop-start))

						

А вот какие результаты я получил на своем компьютере при выполнении этого кода:


Используемая память до выполнения функции: 13.729792
Используемая память после выполнения функции: 13.754368
Заняло 1.6637495591063672e-06 секунд
						

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

Надеюсь после прочтения статьи вы стали лучше понимать для чего используется ключевое слово yield в Python, а так же преимущества использования генераторов в вашей работе.