2008-08-21 11 views
427

Как создать итеративную функцию (или объект-итератор) в python?Построить базовый итератор Python

+3

Здесь два вопроса, оба важных. Как сделать класс итерабельным (т. Е. С чем вы можете перебирать)? И как создать функцию, которая возвращает последовательность с ленивой оценкой? – 2015-07-12 13:26:31

+4

Хорошее упражнение, я думаю, это написать класс, представляющий четные числа (бесконечную последовательность). – 2015-07-12 13:30:02

+1

@ColonelPanic: Хорошо, добавили пример бесконечного числа в [мой ответ] (http://stackoverflow.com/a/7542261/208880). – 2016-02-04 17:25:03

ответ

499

Объекты Iterator в python соответствуют протоколу итератора, что в основном означает, что они обеспечивают два метода: __iter__() и next(). __iter__ возвращает объект итератора и неявно вызывается в начале циклов. Метод next() возвращает следующее значение и неявно вызывается при каждом приращении цикла. next() вызывает исключение StopIteration, когда больше нет значения для возврата, которое неявно захватывается с помощью циклов, чтобы остановить итерацию.

Вот простой пример счетчика:

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

    def __iter__(self): 
     return self 

    def next(self): # Python 3: def __next__(self) 
     if self.current > self.high: 
      raise StopIteration 
     else: 
      self.current += 1 
      return self.current - 1 


for c in Counter(3, 8): 
    print c 

Это будет печатать:

3 
4 
5 
6 
7 
8 

Это проще писать с помощью генератора, так как покрыты в предыдущем ответе:

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

for c in counter(3, 8): 
    print c 

Печатный выход будет таким же. Под капотом объект-генератор поддерживает протокол итератора и делает что-то примерно похожее на счетчик классов.

Статья Дэвида Мерца, Iterators and Simple Generators, является довольно хорошим представлением.

+45

Обратите внимание, что функция `next()` не дает значения `yield`, она` возвращает `их. – 2012-10-17 07:03:44

97

Прежде всего itertools module невероятно полезно для всех видов случаев, в которых итератор был бы полезен, но здесь все, что нужно для создания итератора в Python:

выход

Разве это не круто? Выход можно использовать для замены нормального возврата в функции. Он возвращает объект одинаково, но вместо уничтожения состояния и выхода он сохраняет состояние, когда вы хотите выполнить следующую итерацию. Вот пример его в действие берутся непосредственно из itertools function list:

def count(n=0): 
    while True: 
     yield n 
     n += 1 

Как указано в описании функций (это граф() функции из модуля itertools ...), она производит итератор, возвращает последовательные целые числа, начиная с n.

Generator expressions это целая другая червь червей (удивительные черви!). Они могут использоваться вместо List Comprehension для сохранения памяти (для понимания списка создается список в памяти, который уничтожается после использования, если не назначен переменной, но выражения генератора могут создавать объект генератора ... что является причудливым способом сказать Итератор). Ниже приведен пример определения экспрессии генератора:

gen = (n for n in xrange(0,11)) 

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

Я только что нашел xrange() (это я не видел раньше ...) и добавил его к приведенному выше примеру. xrange() - это итерируемая версия range(), которая имеет то преимущество, что не является предварительным составлением списка. Было бы очень полезно, если бы у вас был гигантский массив данных для перебора и у него было столько памяти, чтобы сделать это.

+17

Начиная с python 3.0 больше нет xrange(), а новый диапазон() ведет себя как старый xrange() – hop 2008-12-18 17:30:08

+6

Вы все равно должны использовать xrange в 2._, потому что 2to3 автоматически переводит его. – Phob 2011-07-22 18:03:40

305

Есть четыре способа построить итерационную функцию:

  • создать генератор (использует yield keyword)
  • использовать выражение генератора (genexp)
  • создать итератор (определяет __iter__ and __next__ (или next в Python 2.x))
  • создать функцию, которую Python может выполнять самостоятельно (defines __getitem__)

Примеры:

# generator 
def uc_gen(text): 
    for char in text: 
     yield char.upper() 

# generator expression 
def uc_genexp(text): 
    return (char.upper() for char in text) 

# iterator protocol 
class uc_iter(): 
    def __init__(self, text): 
     self.text = text 
     self.index = 0 
    def __iter__(self): 
     return self 
    def __next__(self): 
     try: 
      result = self.text[self.index].upper() 
     except IndexError: 
      raise StopIteration 
     self.index += 1 
     return result 

# getitem method 
class uc_getitem(): 
    def __init__(self, text): 
     self.text = text 
    def __getitem__(self, index): 
     result = self.text[index].upper() 
     return result 

Чтобы увидеть все четыре метода в действии:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem: 
    for ch in iterator('abcde'): 
     print ch, 
    print 

Какие результаты в:

A B C D E 
A B C D E 
A B C D E 
A B C D E 

Примечание:

Два генератора типы (uc_gen и uc_genexp) не могут быть reversed(); для простого итератора (uc_iter) нужен магический метод __reversed__ (который должен возвращать новый итератор, который идет назад); и GetItem iteratable (uc_getitem) должен иметь метод __len__ магии:

# for uc_iter 
    def __reversed__(self): 
     return reversed(self.text) 

    # for uc_getitem 
    def __len__(self) 
     return len(self.text) 

Для того, чтобы ответить на вторичный вопрос полковника Panic в о бесконечном лениво оцениваемом итераторе, вот эти примеры, используя каждый из четырех методов выше:

# generator 
def even_gen(): 
    result = 0 
    while True: 
     yield result 
     result += 2 


# generator expression 
def even_genexp(): 
    return (num for num in even_gen()) # or even_iter or even_getitem 
             # not much value under these circumstances 

# iterator protocol 
class even_iter(): 
    def __init__(self): 
     self.value = 0 
    def __iter__(self): 
     return self 
    def __next__(self): 
     next_value = self.value 
     self.value += 2 
     return next_value 

# getitem method 
class even_getitem(): 
    def __getitem__(self, index): 
     return index * 2 

import random 
for iterator in even_gen, even_genexp, even_iter, even_getitem: 
    limit = random.randint(15, 30) 
    count = 0 
    for even in iterator(): 
     print even, 
     count += 1 
     if count >= limit: 
      break 
    print 

что приводит (по крайней мере, для моего образца пробега):

0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 
79

Я вижу, что некоторые из вас делают return self в __iter__. Я просто хотел бы отметить, что __iter__ может быть сам генератор (тем самым устраняя необходимость в __next__ и повышении StopIteration исключения)

class range: 
    def __init__(self,a,b): 
    self.a = a 
    self.b = b 
    def __iter__(self): 
    i = self.a 
    while i < self.b: 
     yield i 
     i+=1 

Конечно здесь можно было бы так же непосредственно сделать генератор, но для более сложных классов это может быть полезным.

3

Это итеративная функция без yield. Это делает использование iter функции и закрытия, которая держит его состояние в изменяемый (list) в области видимости для Python 2.

def count(low, high): 
    counter = [0] 
    def tmp(): 
     val = low + counter[0] 
     if val < high: 
      counter[0] += 1 
      return val 
     return None 
    return iter(tmp, None) 

Для Python 3, состояние закрытия хранится в неизменяемой в области видимости и nonlocal используется в локальной области для обновления переменной состояния.

def count(low, high): 
    counter = 0 
    def tmp(): 
     nonlocal counter 
     val = low + counter 
     if val < high: 
      counter += 1 
      return val 
     return None 
    return iter(tmp, None) 

Тест;

for i in count(1,10): 
    print(i) 
1 
2 
3 
4 
5 
6 
7 
8 
9 
7

Этот вопрос касается истребимых объектов, а не об итераторах. В Python последовательности также повторяются, поэтому один из способов сделать итерируемый класс - заставить его вести себя как последовательность, т. Е. Дать ему __getitem__ и __len__. Я тестировал это на Python 2 и 3.

class CustomRange: 

    def __init__(self, low, high): 
     self.low = low 
     self.high = high 

    def __getitem__(self, item): 
     if item >= len(self): 
      raise IndexError("CustomRange index out of range") 
     return self.low + item 

    def __len__(self): 
     return self.high - self.low 


cr = CustomRange(0, 10) 
for i in cr: 
    print(i)