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