Повторное использование выражений генератора
Выражения генератора - чрезвычайно полезный инструмент, имеющий огромное преимущество по сравнению со списками, заключающимися в том, что он не выделяет память для нового массива.
Проблема, с которой я сталкиваюсь с выражениями генератора, что в итоге заставляет меня писать списки, состоит в том, что я могу использовать такой генератор только один раз:
>>> names = ['John', 'George', 'Paul', 'Ringo']
>>> has_o = (name for name in names if 'o' in name)
>>> for name in has_o:
... print(name.upper())
...
JOHN
GEORGE
RINGO
>>> for name in has_o:
... print(name.lower())
...
>>>
Приведенный выше код иллюстрирует, как выражение генератора может использоваться только один раз. Это, конечно, потому что выражение генератора возвращает экземпляр генератора, а не определяет функцию генератора, которую можно создавать снова и снова.
Есть ли способ клонировать генератор каждый раз, когда он используется, чтобы сделать его многоразовым или заставить синтаксис выражений генератора возвращать функцию генератора, а не один экземпляр?
2 ответа
Сделай это lambda
:
has_o = lambda names: (name for name in names if 'o' in name)
for name in has_o(["hello","rrrrr"]):
print(name.upper())
for name in has_o(["hello","rrrrr"]):
print(name.upper())
lambda
является однострочным и возвращает новый генератор каждый раз. Здесь я решил передать список ввода, но если он исправлен, вам даже не нужен параметр:
names = ["hello","rrrrr"]
has_o = lambda: (name for name in names if 'o' in name)
for name in has_o():
print(name.upper())
for name in has_o():
print(name.upper())
В этом последнем случае, будьте осторожны с тем фактом, что если names
изменяется или переназначается, lambda
использует новый names
объект. Вы можете исправить переназначение имени, используя трюк значения по умолчанию:
has_o = lambda lst=names: (name for name in lst if 'o' in name)
и вы можете исправить последующую модификацию names
используя трюк со значением и копией по умолчанию (бесполезно, если вы думаете, что вашей первой целью было избежать создания списка:)):
has_o = lambda lst=names[:]: (name for name in lst if 'o' in name)
(теперь сделай свой выбор:))
itertools.tee
позволяет сделать несколько итераторов из одной итерации:
from itertools import tee
names = ['John', 'George', 'Paul', 'Ringo']
has_o_1, has_o_2 = tee((name for name in names if 'o' in name), 2)
print('iterable 1')
for name in has_o_1:
print(name.upper())
print('iterable 2')
for name in has_o_2:
print(name.upper())
Выход:
iterable 1
JOHN
GEORGE
RINGO
iterable 2
JOHN
GEORGE
RINGO
Хорошо, народ, вот код, который делает ваш итератор многоразовым. Он автоматически сбрасывается после каждой итерации, поэтому вам не о чем беспокоиться. Насколько это эффективно, ну, это два вызова метода (один next() для tee(), который, в свою очередь, вызывает next() самого итератора), и дополнительный блок try-except поверх исходного итератора. Вы должны решить, допустима ли небольшая потеря скорости, или использовать лямбда для восстановления итератора, как показано в другом ответе.
from itertools import tee
class _ReusableIter:
"""
This class creates a generator object that wraps another generator and makes it reusable
again after each iteration is finished.
It makes two "copies" (using tee()) of an original iterator and iterates over the first one.
The second "copy" is saved for later use.
After first iteration reaches its end, it makes two "copies" of the saved "copy", and
the previous iterator is swapped with the new first "copy" which is iterated over while the second "copy" (a "copy" of the old "copy") waits for the
end of a new iteration, and so on.
After each iteration, the _ReusableIter() will be ready to be iterated over again.
If you layer a _ReusableIter() over another _ReusableIter(), the result can lead you into an indefinite loop,
or provoke some other unpredictable behaviours.
This is caused by later explained problem with copying instances of _ReusableIter() with tee().
Use ReusableIterator() factory function to initiate the object.
It will prevent you from making a new layer over an already _ReusableIter()
and return that object instead.
If you use the _ReusableIter() inside nested loops the first loop
will get the first element, the second the second, and the last nested loop will
loop over the rest, then as the last loop is done, the iterator will be reset and
you will enter the infinite loop. So avoid doing that if the mentioned behaviour is not desired.
It makes no real sense to copy the _ReusableIter() using tee(), but if you think of doing it for some reason, don't.
tee() will not do a good job and the original iterator will not really be copied.
What you will get instead is an extra layer over THE SAME _ReusableIter() for every copy returned.
TODO: A little speed improvement can be achieved here by implementing tee()'s algorithm directly into _ReusableIter()
and dump the tee() completely.
"""
def __init__ (self, iterator):
self.iterator, self.copy = tee(iterator)
self._next = self.iterator.next
def reset (self):
self.iterator, self.copy = tee(self.copy)
self._next = self.iterator.next
def next (self):
try:
return self._next()
except StopIteration:
self.reset()
raise
def __iter__ (self):
return self
def ReusableIter (iterator):
if isinstance(iterator, _ReusableIter):
return iterator
return _ReusableIter(iterator)
Usage:
>>> names = ['John', 'George', 'Paul', 'Ringo']
>>> has_o = ReusableIter(name for name in names if 'o' in name)
>>> for name in has_o:
>>> print name
John
George
Ringo
>>> # And just use it again:
>>> for name in has_o:
>>> print name
John
George
Ringo
>>>