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

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

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

Различия между списком и генератором

В следующем скрипте мы создадим и список, и генератор и попытаемся увидеть, чем они отличаются. Сначала создадим простой список и проверим его тип:

# Creating a list using list comprehension
squared_list = [x**2 for x in range(5)]

# Check the type
type(squared_list)

При запуске этого кода вы должны увидеть, что отображаемый тип будет «список».

Теперь давайте переберем все элементы в squared_list.

# Iterate over items and print them
for number in squared_list:
    print(number)

Приведенный выше скрипт даст следующие результаты:

$ python squared_list.py 
0
1
4
9
16

Теперь давайте создадим генератор и выполним ту же самую задачу:

# Creating a generator
squared_gen = (x**2 for x in range(5))

# Check the type
type(squared_gen)

Чтобы создать генератор, вы начинаете точно так же, как и с пониманием списка, но вместо квадратных скобок вам нужно использовать круглые скобки. В приведенном выше скрипте в качестве типа переменной 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 используется для возврата числа в кубе внутри цикла for.

Теперь, когда вызывается функция cube_number, возвращается генератор, что мы можем проверить, запустив код:

$ python cubes_gen.py 
<generator object cube_numbers at 0x1087f1230>

Несмотря на то, что мы вызвали функцию cube_numbers, она фактически не выполняется в этот момент времени, и в памяти еще нет никаких элементов.

Чтобы заставить функцию выполняться и, следовательно, следующий элемент из генератора, мы используем встроенный метод next. Когда вы вызываете следующий итератор в генераторе в первый раз, функция выполняется до тех пор, пока не встретится ключевое слово yield. Как только yield найден, переданное ему значение возвращается вызывающей функции, а функция генератора приостанавливается в своем текущем состоянии.

Вот как вы получаете значение от своего генератора:

next(cubes)

Вышеупомянутая функция вернет «1». Теперь, когда вы снова вызываете next в генераторе, функция cube_numbers возобновит выполнение с того места, где она остановилась ранее на yield. Функция будет продолжать выполняться, пока снова не найдет yield. Следующая функция будет продолжать возвращать кубическое значение одно за другим, пока все значения в списке не будут повторены.

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

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

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

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

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

В приведенном ниже коде мы напишем функцию, которая возвращает список, содержащий 1 миллион фиктивных объектов автомобиля. Мы рассчитаем объем памяти, занятой процессом, до и после вызова функции (которая создает список).

Взгляните на следующий код:

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

# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))

# Call the car_list function and time how long it takes
t1 = time.clock()
cars = car_list(1000000)
t2 = time.clock()

# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))

print('Took {} seconds'.format(t2-t1))

Примечание. Возможно, вам придется установить 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

# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))

# Call the car_list_gen function and time how long it takes
t1 = time.clock()
for car in car_list_gen(1000000):
    pass
t2 = time.clock()

# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))

print('Took {} seconds'.format(t2-t1))

Здесь мы должны использовать цикл for car in car_list_gen (1000000), чтобы гарантировать, что все 1000000 автомобилей действительно сгенерированы.

Следующие результаты были получены при выполнении вышеуказанного скрипта:

$ python perf_gen.py 
Memory before list is created: 8
Memory after list is created: 40
Took 1.365244 seconds

Из выходных данных вы можете видеть, что при использовании генераторов разница в памяти намного меньше, чем раньше (от 8 МБ до 40 МБ), поскольку генераторы не сохраняют элементы в памяти. Кроме того, время, затраченное на вызов функции генератора, также было немного быстрее – 1,37 секунды, что примерно на 14% быстрее, чем создание списка.

Заключение

Генераторы Python – отличный способ повысить производительность программ, и они очень просты в использовании, но понимание того, когда их использовать, является проблемой для многих начинающих программистов.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *