Генераторы

В Python просто генераторы и генераторы списков - разные вещи. Здесь есть проблема перевода с английского. То, что мы привыкли называть генератором списка, в английском варианте звучит как "list comprehension" и к генераторам никакого отношения не имеет.

Слово "comprehension" (понимание, осмысление) оказывается как бы не в тему при переводе на русский. Получается что-то вроде "понимание списка". Поэтому мы говорим "генератор списка", понимая под словом "генератор" не объект, а синтаксическую конструкцию, которая генерирует, то есть создает, список.

С другой стороны, объекты-генераторы - это особые объекты-функции, которые между вызовами сохраняют свое состояние. В цикле for они ведут себя подобно итерируемым объектам, к которым относятся списки, словари, строки и др. Однако генераторы поддерживают метод __next__(), а значит являются разновидностью итераторов.

Быстрым способом создания относительно простых объектов-генераторов являются генераторные выражения - generator expressions. Синтаксис этих выражений похож на синтаксис генераторов списков. Однако они возвращают разные типы объектов. Первый - объект-генератор. Второй - список.

Сначала рассмотрим генераторы списков, чтобы привыкнуть к синтаксической конструкции.

Генераторы списков

В Python генераторы списков позволяют создавать и быстро заполнять списки.

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

>>> a = [1, 2, 3]
>>> b = [i+10 for i in a]
>>> a
[1, 2, 3]
>>> b
[11, 12, 13]

В примере выше генератором списка является выражение [i+10 for i in a]. Здесь a - итерируемый объект. В данном случае это другой список. Из него извлекается каждый элемент в цикле for. Перед for описывается действие, которое выполняется над элементом перед его добавлением в новый список.

Обратите внимание, что генератор создает новый список, а не изменяет существующий. Если надо изменить текущую переменную, ей надо присвоить новое значение:

>>> a = [1, 2, 3]
>>> a = [i+10 for i in a]
>>> a
[11, 12, 13]

Генераторы списков относятся к разряду "синтаксического сахара" языка программирования Python. Другими словами, без них можно обойтись:

>>> for index, value in enumerate(a):
...     a[index] = value + 10
...
>>> a
[11, 12, 13]

Если в программе может быть несколько ссылок на список, генераторами надо пользоваться осторожно:

>>> ls0 = [1,2,3]
>>> ls1 = ls0
>>> ls1.append(4)
>>> ls0
[1, 2, 3, 4]
>>> ls1 = [i+1 for i in ls1]
>>> ls1
[2, 3, 4, 5]
>>> ls0
[1, 2, 3, 4]

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

Перебираемым в цикле for объектом может быть быть не только список. В примере ниже в список помещаются строки файла.

>>> lines = [line.strip() for line in open('text.txt')]
>>> lines
['one', 'two', 'three']

В генератор списка можно добавить условие:

>>> from random import randint
>>> nums = [randint(10, 20) for i in range(10)]
>>> nums
[18, 17, 11, 11, 15, 18, 11, 20, 10, 19]
>>> nums = [i for i in nums if i%2 == 0]
>>> nums
[18, 18, 20, 10]

Генераторы списков могут содержать вложенные циклы:

>>> a = "12"
>>> b = "3"
>>> c = "456"
>>> comb = [i+j+k for i in a for j in b for k in c]
>>> comb
['134', '135', '136', '234', '235', '236']

Генераторы словарей и множеств

Если в выражении генератора списка заменить квадратные скобки на фигурные, то можно получить не список, а словарь:

>>> a = {i:i**2 for i in range(11,15)}
>>> a
{11: 121, 12: 144, 13: 169, 14: 196}

При этом синтаксис выражения до for должен быть соответствующий словарю, то есть включать ключ и через двоеточие значение. Если этого нет, будет сгенерировано множество:

>>> a = {i for i in range(11,15)}
>>> a
set([11, 12, 13, 14])
>>> b = {1, 2, 3}
>>> b
set([1, 2, 3])

Генераторы

Выражения, создающие объекты-генераторы, похожи на выражения, генерирующие списки, словари и множества за одним исключением. Чтобы создать генераторный объект, надо использовать круглые скобки:

>>> a = (i for i in range(2, 8))
>>> a
<generator object <genexpr> at 0x7efc88787910>
>>> for i in a:
...     print(i)
...
2
3
4
5
6
7

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

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

Выражение, создающее генератор, это сокращенная запись следующего:

>>> def func(start, finish):
...     while start < finish:
...             yield start * 0.33
...             start += 1
...
>>> a = func(1, 4)
>>> a
<generator object func at 0x7efc88787a50>
>>> for i in a:
...     print(i)
...
0.33
0.66
0.99

Функция, содержащая yield возвращает объект-генератор, а не выполняет свой код сразу. Тело функции исполняется при каждом вызове метода __next__(). В цикле for это делается автоматически. При этом функция сохраняет значения переменных от предыдущего вызова.

Если нет необходимости использовать функцию многократно, проще использовать выражение:

>>> b = (i*0.33 for i in range(1,4))
>>> b
<generator object <genexpr> at 0x7efc88787960>
>>> for i in b:
...     print(i)
...
0.33
0.66
0.99