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

Ключевое слово yield в Python используется для создания генераторов. Генератор [/ python-generators /] - это тип коллекции, которая производит элементы на лету и может быть повторена только один раз. Используя генераторы, вы можете улучшить производительность своего приложения и потреблять меньше памяти по сравнению с обычными коллекциями, что обеспечивает хороший прирост производительности. В этой статье мы объясним, как использовать ключевое слово 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 , в отличие от 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 функция cube_number, возвращается генератор, что мы можем проверить, запустив код:

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

Несмотря на то, что мы cube_numbers функцию 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 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 
 
 # 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% быстрее, чем создание списка.

Заключение

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus