В этой главе мы изучим итераторы, генераторы и декораторы.
Итераторы
Объекты-итераторы в 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