Язык программирования Python

Как использовать yield и генераторы в Python

Генераторы – это потрясающая особенность Python и важный шаг в освоении языка. Поняв их, вы уже не сможете без них обойтись.

Напомним об итерациях

Когда вы читаете элементы по одному из списка, это называется итерацией:

lst = [1, 2, 3] >>> for i in lst : ... print(i) 1 2 3
Code language: PHP (php)

И когда мы используем списковое включение , мы создаем список, то есть итерабельность.

В цикле for мы берем его элементы один за другим, и таким образом выполняем итерацию:

lst = [x*x for x in range(3)] >>> for i in lst : ... print(i) 0 1 4
Code language: PHP (php)

Каждый раз, когда вы можете использовать “for… in…” для чего-либо, это итерабельный объект: списки, строки, файлы…

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

Генераторы

Таким же образом можем создавать генерируемые значения:

generator = (x*x for x in range(3)) >>> for i in generator: ... print(i) 0 1 4
Code language: PHP (php)

Единственное отличие от предыдущего варианта заключается в том, что вместо [] используется ().

Но вы не можете прочитать генератор второй раз, потому что принцип работы генераторов заключается в том, что они генерируют все на лету: здесь он вычисляет 0, потом забывает его, потом вычисляет 1, потом забывает его, потом вычисляет 4. Все это по очереди.

Ключевое слово yield

yield – это слово, используемое вместо return. Только в этом случае мы получим генератор.

>>> def CreateGenerator() : ... mylist = range(3) ... for i in mylist: ... yield i*i ... >>> generator = CreateGenerator() # создание генератора >>> print(generator) # генератор - это объект ! < generator object CreateGenerator at 0x2b484b9addc0> >>> for i in generator: ... print(i) 0 1 4
Code language: PHP (php)

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

Секрет мастеров дзен, которые обрели понимание yield, заключается в том, что когда вы вызываете функцию, код функции не выполняется. Вместо этого функция вернет объект генератора.

Это нелегко понять, поэтому прочитайте эту часть несколько раз.

CreateGenerator() не выполняет код CreateGenerator.

CreateGenerator() возвращает объект генератора.

На самом деле, пока вы не трогаете генератор, ничего не происходит. Затем, как только мы начинаем итерировать генератор, запускается код функции.

При первом выполнении кода он начнет с начала функции, дойдет до yield и вернет первое значение.

Затем, при каждом новом цикле, код будет выполняться с того места, где он остановился (да, Python сохраняет состояние кода генератора между каждым вызовом), и выполнять код снова, пока не встретится с yield. Поэтому в нашем случае он будет зациклен.

Так будет продолжаться до тех пор, пока код не перестанет удовлетворять условию yield, и, следовательно, не будет больше возвращаться значение.

В этом случае генератор считается окончательно пустым.

Его нельзя перемотать, нужно создать другой.

Причина, по которой код больше не удовлетворяет условию yield, выбирается вами: условие if/else, цикл, рекурсия…

Конкретный пример

yield не только экономит память, но и скрывает сложность алгоритма за классическим API итерации.

Предположим, у вас есть функция, которая извлекает слова длиной более 3 символов из всех файлов в папке.

Он может выглядеть следующим образом:

import os def extract_words(work_file): for file in os.listdir(work_file): with open(os.path.join(work_file, file)) as f: for line in f: for word in line.split(): if len(word) > 3: yield word
Code language: JavaScript (javascript)

У вас есть алгоритм, который полностью скрывает свою сложность, потому что с точки зрения пользователя он просто делает это:

for word in extract_words(work_file): print word
Code language: PHP (php)

И для него это открыто.

Более того, он может использовать все инструменты, которые мы обычно используем для итераций.

Все функции, которые принимают итерабельные значения, принимают результат функции в качестве параметра благодаря магии duck typing.

Таким образом, создается прекрасный набор инструментов.

Контроллер yield

>>> class carsDistributor(): stock = True def turnon(self): while self.stock: yield "Автомобиль" ...

Пока есть в наличии, вы можете получить столько автомобилей, сколько захотите.

>>> distributor_down_street = carsDistributor() >>> distributor = distributor_down_street.turnon() >>> print distributor.next() Автомобильобиль4)]) ['Автомобиль', 'Автомобиль', 'Автомобиль', 'Автомобиль']
Code language: PHP (php)

Как только запасов не останется…

>>> distributor_down_street.stock = False >>> distributor.next() Traceback (most recent call last): File "<ipython-input-22-389e61418395>", line 1, in <module> distributor.next() StopIteration < type 'exceptions.StopIteration'>
Code language: JavaScript (javascript)

И это справедливо для любого нового генератора:

>>> distributor = distributor_down_street.turnon() >>> distributor.next() Traceback (most recent call last): File "<ipython-input-24-389e61418395>", line 1, in <module> distributor.next() StopIteration
Code language: JavaScript (javascript)

itertools: ваш новый любимый модуль

С генераторами нужно работать с учетом их природы: вы можете прочитать их только один раз и не можете заранее определить их длину.

itertools – это модуль, специализирующийся на этом: map, zip, slice…

Он содержит функции, которые работают со всеми итерациями, включая генераторы.

Построим две итерабельные строки и возьмем первые 10 символов?

>>> import itertools >>> d = carsDistributor().turnon() >>> generator = itertools.chain("12345", d) >>> generator = itertools.islice(generator, 0, 10) >>> for x in generator: ... print x ... 1 2 3 4 5 Автомобильобиль Автомобильобиль Автомобильобиль Автомобильобиль cАвтомобильобиль
Code language: JavaScript (javascript)

За кулисами итерации

Под капотом все iterables используют генератор под названием ” итератор”. Вы можете получить его, используя функцию iter() для итератора:

>>> iter([1, 2, 3]) < listiterator object at 0x7f58b9735dd0> >>> iter((1, 2, 3)) < tupleiterator object at 0x7f58b9735e10> >>> iter(x*x for x in (1, 2, 3)) < generator object at 0x7f58b9723820>
Code language: HTML, XML (xml)

Итераторы имеют метод next(), который возвращает значение для каждого вызова метода.

Когда больше нет значений, они выбрасывают исключение StopIteration:

>>> gen = iter([1, 2, 3]) >>> gen.next() 1 >>> gen.next() 2 >>> gen.next() 3 >>> gen.next() Traceback (most recent call last): File "< stdin>", line 1, in < module> StopIteration
Code language: JavaScript (javascript)

Для всех тех, кто думает, что я выдумываю, когда говорю, что в Python мы используем исключения для управления потоком программы: это механизм внутреннего цикла в Python.

Циклы for используют функцию iter() для создания генератора, а затем перехватывают исключение для остановки. В каждом цикле for вы, сами того не зная, вызываете исключение.

Для справки, текущая реализация заключается в том, что iter() вызывает метод iter() на объекте, переданном в качестве параметра.

Это означает, что вы можете создавать свои собственные итерационные таблицы:

>>> class MyIterNothingAboutMe(object): ... def __iter__(self): ... yield 'Python' ... yield "это" ... yield 'мощь' ... >>> gen = iter(MyIterNothingAboutMe()) >>> gen.next() 'Python' >>> gen.next() 'это' >>> gen.next() 'мощь' >>> gen.next() Traceback (most recent call last): File "< stdin>", line 1, in < module> StopIteration >>> for x in MyIterNothingAboutMe(): ... print x ... Python это мощь
Code language: HTML, XML (xml)

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

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