Понимание yield в Python
Ключевое слово yield в Python используется для создания генераторов. Генератор – это коллекция, которая продуцирует элементы на лету и может быть повторена только один раз. С помощью генераторов можно повысить производительность приложения и снизить потребление памяти по сравнению с обычными коллекциями.
В этой статье будет рассказано, как использовать ключевое слово yield в Python и как именно оно работает. Но сначала давайте поймём разницу между простым списком и генератором, а затем посмотрим, как yield можно использоваться для создания более сложных генераторов.
Различия между списком и генератором
В следующих примерах будет создан список и генератор, чтобы увидеть их отличия. Сначала создадим простой список и проверим его тип:
# Применение генератора списка
squared_list = [x**2 for x in range(5)]
# Проверяем тип
type(squared_list)
При запуске этого кода, программа вернёт результат "list". Теперь давайте переберём все элементы в списке squared_list.
# Выполните итерации по элементам и их распечатка
for number in squared_list:
print(number)
Приведенный выше сценарий вернёт следующие результаты:
$ python squared_list.py
0
1
4
9
16
Теперь создадим генератор и выполнить ту же задачу:
# Создание генератора
squared_gen = (x**2 for x in range(5))
# Проверяем тип
type(squared_gen)
Генератор создаётся подобно коллекции списка, но вместо квадратных скобок нужно использовать круглые скобки. Приведенный выше сценарий вернёт значение "generator" как тип переменной squared_gen. Теперь давайте переберём элементы генератора с помощью цикла for.
for number in squared_gen:
print(number)
Выход будет:
$ python squared_gen.py
0
1
4
9
16
Выходные данные такие же, как у списка. Так в чем же разница? Одно из главных отличий заключается в том, как в список и генератор хранят элементы в памяти. Списки хранят все элементы в памяти сразу, тогда как генераторы "создают" каждый элемент на лету, отображая их, а затем перемещаются к следующему элементу, удаляя предыдущий элемент из памяти.
Один из способов проверить это, узнать длину как списка, так и генератора, который только что создали. Функции len(squared_list) вернет 5, а len(squared_gen) выдаст ошибку отсутствия длины у генератора. Кроме того, список можно перебирать столько раз, сколько захотите, но генератор можно перебирать только один раз. Для повторной итерации необходимо создать генератор снова.
Использование ключевого слова yield
Теперь мы знаем разницу между простыми коллекциями и генераторами, давайте посмотрим, как yield может помочь нам определить генератор.
В предыдущих примерах был создан генератор неявно, используя синтаксис генераторов списков. Однако в более сложных сценариях необходимо создавать функции, которые возвращают генератор. Ключевое слово yield, в отличие от оператора return, используется для превращения обычной функции Python в генератор. Оно используется в качестве альтернативы одновременному возвращению целого списка.
Опять же, давайте сначала посмотрим, что возвращает наша функция, если не использовать ключевое слово yield. Выполните следующий сценарий:
def cube_numbers(nums):
cube_list = []
for i in nums:
cube_list.append(i**3)
return cube_list
cubes = cube_numbers([1, 2, 3, 4, 5])
print(cubes)
В этом скрипте создается функция cube_numbers, которая принимает список чисел, вычисляет их куб и возвращает вызывающему объекту список целиком. При вызове этой функции список кубов возвращается и сохраняется в переменную cubes. Как видно из вывода, возвращаемые данные – это список целиком:
$ python cubes_list.py
[1, 8, 27, 64, 125]
Теперь, изменим сценарий, так чтобы он возвращал генератор.
def cube_numbers(nums):
for i in nums:
yield(i**3)
cubes = cube_numbers([1, 2, 3, 4, 5])
print(cubes)
В приведенном выше скрипте функция cube_numbers возвращает генератор вместо списка кубов чисел. Создать генератор с помощью ключевого слова yield очень просто. Здесь нам не нужна временная переменная cube_list для хранения куба числа, поэтому даже наш метод cube_numbers проще. Кроме того, не используется оператор return, но вместо него используется слово yield для возвращения куба числа внутри цикла.
Теперь, когда функция cube_number возвращает генератор, проверим его, запустив код:
$ python cubes_gen.py
<generator object cube_numbers at 0x3457f1567>
Несмотря на то, что был произведён вызов функции cube_numbers, она фактически не выполняется на данный момент времени, и в памяти еще нет элементов.
Получение значение из генератора:
next(cubes)
Вышеуказанная функция возвратит "1". Теперь, когда снова вызывается next генератора, функция cube_numbers возобновит выполнение с того места, где она ранее остановилась на yield. Функция будет продолжать выполняться до тех пор, пока снова не найдет yield. Следующая функция будет продолжать возвращать значение куба по одному, пока все значения в списке не будут проитерированы.
Как только все значения будут проитерированы, следующий вызов функции создаст исключение StopIteration. Важно отметить, что генератор кубов не хранит какие-либо элементы в памяти, а значения в кубе вычисляются во время выполнения, возвращаются и забываются. Используется только дополнительная память для хранения данных состояния самого генератора, которая, как правило, гораздо меньше, чем полный список. Это делает генераторы идеально подходящими для ресурсоемких задач.
Вместо того, чтобы использовать next итератора, можно также использовать цикл for для перебора значений генераторов. При использовании цикла for за кулисами вызывается next итерации, пока не будут возвращены все элементы генератора.
Оптимизация производительности
Как упоминалось ранее, генераторы очень удобны, когда дело доходит до задач, активно расходующих память, так как они не хранят все элементы коллекции в памяти, а генерируют элементы на лету и удаляют их, как только итератор переходит к следующему элементу.
В предыдущих примерах разница в производительности простого списка и генератора не была видна, так как размеры списка были малы. В этом разделе рассмотрим некоторые примеры, где можно сравнить производительность списков и генераторов.
В приведенном ниже коде представлена функция, которая возвращает список, содержащий 1 миллион фиктивных объектов car. Рассчитаем память, процессорное время до и после вызова функции.
Взглянем на следующий код:
import time
import random
import os
import psutil
car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']
def car_list(cars):
all_cars = []
for i in range (cars):
car = {
'id': i,
'name': random.choice(car_names),
'color': random.choice(colors)
}
all_cars.append(car)
return all_cars
# замеряем потребление памяти
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))
# Вызов функции car_list и время, сколько времени это занимает
t1 = time.clock()
cars = car_list(1000000)
t2 = time.clock()
# замеряем потребление памяти
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))
print('Took {} seconds'.format(t2-t1))
Примечание: возможно, придется выполнить pip install psutil, чтобы этот код был работоспособен.
На компьютере автора статьи получены следующие результаты (в вашем случае результаты могут быть иными):
$ python perf_list.py
Memory before list is created: 8
Memory after list is created: 334
Took 1.584018 seconds
До создания списка потребляемая память процесса составляла 8 МБ, а после создания списка с 1 миллионом элементов занимаемая память подскочила до 334 МБ. Кроме того, для создания списка было затрачено 1.58 секунды.
Теперь, повторим процесс, но заменив список на генератор. Выполним следующий сценарий:
import time
import random
import os
import psutil
car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']
def car_list_gen(cars):
for i in range (cars):
car = {
'id':i,
'name':random.choice(car_names),
'color':random.choice(colors)
}
yield car
# замеряем потребление памяти
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))
# Вызов функции car_list_gen и время, сколько времени занимает
t1 = time.clock()
cars = car_list_gen(1000000)
t2 = time.clock()
# замеряем потребление памяти
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))
print('Took {} seconds'.format(t2-t1))
Результаты при выполнении вышеуказанного скрипта:
$ python perf_gen.py
Memory before list is created: 8
Memory after list is created: 8
Took 3e-06 seconds
Из выходных данных видно, что при использовании генераторов разница в потреблении памяти незначительна (она остается на уровне 8 МБ), так как генераторы не хранят элементы в памяти. Кроме того, время, затраченное на вызов функции генератора, составило всего 0,000003 секунды – это намного меньше затраченного времени, по сравнению с списком.
Вывод
Надеюсь, после прочтения этой статьи вы лучше стали понимать ключевое слово yield, в том числе, как его использовать, для чего оно используется. Генераторы Python – отличный способ улучшить производительность программ, и они очень просты в использовании, но понимание того, когда их использовать, является проблемой для многих начинающих программистов.