В этой главе мы изучим итераторы, генераторы и декораторы.

Итераторы

Объекты-итераторы в Python должны поддерживать два метода, следуя протоколу итератора.

__iter__ возвращает сам объект-итератор. Это используется в for и in операторах.

__next__ метод возвращает следующее значение из итератора. Если больше нет элементов для возврата, то он должен вызвать исключение StopIteration.

class Counter(object):
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        'Возвращает сам себя как объект-итератор'
        return self

    def __next__(self):
        'Возвращает следующее значение, пока current меньше high'
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

Теперь мы можем использовать этот итератор в нашем коде.

>>> c = Counter(5,10)
>>> for i in c:
...   print(i, end=' ')
...
5 6 7 8 9 10

Помните, что объект-итератор можно использовать только один раз. Это означает, что после того, как он вызовет исключение StopIteration, он будет продолжать вызывать это исключение.

>>> c = Counter(5,6)
>>> next(c)
5
>>> next(c)
6
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in next
StopIteration
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in next
StopIteration

Используя итератор в цикле for, как мы видели, следующий пример пытается показать код за сценой.

>>> iterator = iter(c)
>>> while True:
...     try:
...         x = iterator.__next__()
...         print(x, end=' ')
...     except StopIteration as e:
...         break
...
5 6 7 8 9 10

Генераторы

В этом разделе мы изучаем генераторы Python. Они были введены в Python 2.3. Это более простой способ создания итераторов с использованием ключевого слова yield в функции.

>>> def my_generator():
...     print("Inside my generator")
...     yield 'a'
...     yield 'b'
...     yield 'c'
...
>>> my_generator()
<generator object my_generator at 0x7fbcfa0a6aa0>

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

>>> for char in my_generator():
...     print(char)
...
Inside my generator
a
b
c

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

def counter_generator(low, high):
    while low <= high:
       yield low
       low += 1

>>> for i in counter_generator(5,10):
...     print(i, end=' ')
...
5 6 7 8 9 10

Внутри цикла while, когда он достигает оператора yield, возвращается значение low, и состояние генератора приостанавливается. Во время следующего вызова next генератор возобновляет работу с того места, где он был приостановлен, и значение low увеличивается на единицу. Он продолжает выполнение цикла while и снова достигает оператора yield.

Когда вы вызываете функцию-генератор, она возвращает объект *generator*. Если вы вызовете *dir* на этом объекте, вы обнаружите, что он содержит методы __iter__ и *__next__* среди других методов.

   >>> c = counter_generator(5,10)
   >>> dir(c)
   ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__',
'__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'close', 'gi_code', 'gi_frame', 'gi_running', 'send', 'throw']

Мы в основном используем генераторы для ленивых вычислений. Таким образом, генераторы становятся хорошим подходом для работы с большим объемом данных. Если вы не хотите загружать все данные в память, вы можете использовать генератор, который будет передавать вам каждую часть данных за раз.

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

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

>>> def infinite_generator(start=0):
...     while True:
...         yield start
...         start += 1
...
>>> for num in infinite_generator(4):
...     print(num, end=' ')
...     if num > 20:
...         break
...
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

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

>>> g = my_generator()
>>> for c in g:
...     print(c)
...
Inside my generator
a
b
c
>>> for c in g:
...     print(c)
...

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

>>> class Counter(object):
...     def __init__(self, low, high):
...         self.low = low
...         self.high = high
...     def __iter__(self):
...          counter = self.low
...          while self.high >= counter:
...              yield counter
...              counter += 1
...
>>> gobj = Counter(5, 10)
>>> for num in gobj:
...     print(num, end=' ')
...
5 6 7 8 9 10
>>> for num in gobj:
...     print(num, end=' ')
...
5 6 7 8 9 10

Выражения-генераторы

Выражения-генераторы

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

Например, мы попробуем суммировать квадраты всех чисел от 1 до 99.

>>> sum([x*x for x in range(1,10)])

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

Мы можем сэкономить использование памяти, используя выражение-генератор.

sum(x*x for x in range(1,10))

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

>>> sum(x*x for x in range(1,10))
285
>>> g = (x*x for x in range(1,10))
>>> g
<generator object <genexpr> at 0x7fc559516b90>

Мы можем объединять генераторы или выражения-генераторы. В следующем примере мы прочитаем файл */var/log/cron* и проверим, запущена ли какая-либо конкретная задача (в примере мы ищем anacron) успешно или нет.

Мы можем сделать то же самое с помощью командной оболочки tail -f /var/log/cron |grep anacron

>>> jobtext = 'anacron'
>>> all = (line for line in open('/var/log/cron', 'r') )
>>> job = ( line for line in all if line.find(jobtext) != -1)
>>> text = next(job)
>>> text
"May  6 12:17:15 dhcp193-104 anacron[23052]: Job `cron.daily' terminated\n"
>>> text = next(job)
>>> text
'May  6 12:17:15 dhcp193-104 anacron[23052]: Normal exit (1 job run)\n'
>>> text = next(job)
>>> text
'May  6 13:01:01 dhcp193-104 run-parts(/etc/cron.hourly)[25907]: starting 0anacron\n'

Вы можете написать цикл for для строк.

Замыкания

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

>>> def add_number(num):
...     def adder(number):
...         'adder is a closure'
...         return num + number
...     return adder
...
>>> a_10 = add_number(10)
>>> a_10(21)
31
>>> a_10(34)
44
>>> a_5 = add_number(5)
>>> a_5(3)
8

adder — это замыкание, которое добавляет заданное число к предопределенному.

Декораторы

Декоратор — это способ динамически добавлять новое поведение к некоторым объектам. Мы достигаем этого в Python с помощью замыканий.

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

>>> def my_decorator(func):
...     def wrapper(*args, **kwargs):
...         print("Перед вызовом")
...         result = func(*args, **kwargs)
...         print("После вызова")
...         return result
...     return wrapper
...
>>> @my_decorator
... def add(a, b):
...     "Наша функция сложения"
...     return a + b
...
>>> add(1, 3)
Перед вызовом
После вызова
4

Перейти к следующему уроку →