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