Рубрики
Без рубрики

Понимание ключевого слова Python “yield”

Автор оригинала: Scott Robinson.

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

В этой статье мы объясним, как использовать ключевое слово 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

Результат такой же, как и в списке. Так в чем же разница? Одно из главных отличий заключается в том, как список и генераторы хранят элементы в памяти. Списки хранят все элементы в памяти сразу, тогда как генераторы “создают” каждый элемент на лету, отображают его, а затем переходят к следующему элементу, отбрасывая предыдущий элемент из памяти.

Один из способов проверить это-проверить длину как списка, так и генератора, который мы только что создали. lens(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 

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

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

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

next(cubes)

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

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

Вместо того чтобы всегда использовать итератор 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

# 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))

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

Вывод

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