Итераторы и генераторы. Итераторы и генераторы. Итераторы и генераторы.

Итераторы и генераторы.

Open in new window

Генераторы и итераторы.

Итераторы

We use for statement for looping over a list.

Для того чтобы пройти в цикле по коллекции, обычно используется цикл for.

По списку.

for i in [1, 2, 3, 4]:
    print i,

По строке.

for c in "python":
    print c

По файлу.

for line in open("a.txt"):
    print line,

Что если по какой либо причине мы не хотим использовать цикл for для прохода по коллекции?

Для этого нужно иметь специальный метод (next), в котором по порядку возвращать ее элементы до тех пор пока не достигнем конца коллекции.

Например так:

with open('/etc/passwd') as f:
    try:
        while True:
            line = next(f)
            print(line, end='')
    except StopIteration:
        pass


with open('/etc/passwd') as f:
     while True:
         line = next(f, None)
         if line is None:
             break
         print(line, end='')

К щастью, такая существует и она встроена в Python.

Но приминима только к обьектам-итераторам.

При вызове функции next(obj) задействуется внутренний метод итератора obj.next().

Такой код вызовет исключение ‘list’ object is not an iterator.

mlst = [1,2,3,4]

try:
    next(mlst)
except Exception as e:
    print(e)

Функция iter()

Чтобы превратить объект в итератор, необходимо использовать встроенную функцию iter().

Эта функция, подобно next(), вызывает встроенный метод iter оъекта.

mlst = [1,2,3,4]

mlistIter = iter(mlst)  # превращаем в итератор 
print(next(mlistIter))
print(next(mlistIter))

>> 1
>> 2

Так же можно использовать mlistIter в цикле:

for i in mlistIter:
    print(i)

Возникает вопрос. Каким образом работает этот код если mlst не является изначально итератором?

mlst = [1,2,3,4]
for i in mlst:
    print(i)

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

В Python есть и встроенная функция iter, котрая принимает параметром объект и превращает его в итерируемый. После этого такой объект может быть использован в конструкциях цикла for или while.

По сути, сам процесс прохода цикла можно представить как последовательность вызовов ф-ции next() итератора.

>>> items = [1, 2, 3]
>>> # Get the iterator
>>> it = iter(items)     # Invokes items.__iter__()
>>> # Run the iterator
>>> next(it)             # Invokes it.__next__()
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Этот код демонстрирует базовый механизм итерации внутри цикловых конструкций.

Используя механизм неявного вызова iter можно создавать свои собственные итераторы.

class Myiter:
    def __init__(self, value):
        self._value = value

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def __iter__(self):
        print('Iter execution')
        return iter(self._value)



d = Myiter([1,2,3])
print(d)
for i in d:
    print(i)

>>Node([1, 2, 3])
>>Iter execution
>>1
>>2
>>3

В этом примере мы продемонстрировали еще одну особенность: при вызове print(obj) неявно вызывается внутренний метод obj.repr().

Итак, протокол итерирования Python предполагает использование iter() для получения объекта, который реализует метод next() для работы в цикле.

Усложним наш итератор, добавив контейнер self._children и возможность добавления элементов в него.

class Myiter:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

# Example
if __name__ == '__main__':
    root = Myiter(0)
    root.add_child('hello')
    root.add_child('world')
    for ch in root:
        print(ch)

>> Outputs hello world

Генераторы.

Нам уже известны такие встроенные функции как range() или reversed() и т.д.

Они возвращают нам итераторы (итерируемые объекты).

Что делать если мы хоти создать аналогичную функцию?

Исли попробовать так:

def mygen():
    return [1,2,3,4]

for i in mygen():
    print(i)

Этот код работает, но отсутствует возможность динамически формировать последовательность внутри функции.

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

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

Оператор yield

Для этого существует специальный опреатор yeld, который должен возвратить значение, попадаемое в каждую итерацию цикла.

Следующий пример демонстрирует как это можно сделать.

def frange(start, stop, increment):
    x = start
    while x < stop:
        yield x
        x += increment

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

Такая функция, которая использует оператор yield называется генератором.

Интересной особенностью является тот факт что внутри этого генератора сохраняется позиция очередного предыдущего yield.

def mygen():
    yield 1
    yield 2
    yield 3

for i in mygen():
    print(i)

Т.е. в цикле функция не выполняется каждый раз с начала а только с места предыдущего возврата значения yield.

Таким образом нашу задачу по разделению массива можно решить так.

supermas = [[1,2,3],[4,5,6],[7,8,9]]

def genmas():
    for i in supermas:
        yield i

for i in genmas():
    print(i)

>>[1, 2, 3]
>>[4, 5, 6]
>>[7, 8, 9]

На практике такой подход часто используется для прохода по большой таблице базы данных.

При этом вместо одного большого SQL запроса, создается генератор, который делает много мелких, запоминая текущую позицию (курсор) внутри генератора.

Ссылки на источники.

http://nvie.com/posts/iterators-vs-generators/ http://anandology.com/python-practice-book/iterators.html http://chimera.labs.oreilly.com/books/1230000000393/ch04.html

Other topics