Pregunta

¿Cómo se crearía una función iterativa (u objeto iterador) en Python?

¿Fue útil?

Solución

Los objetos iteradores en Python se ajustan al protocolo iterador, lo que básicamente significa que proporcionan dos métodos: __iter__() y next().El __iter__ devuelve el objeto iterador y se llama implícitamente al inicio de los bucles.El next() El método devuelve el siguiente valor y se llama implícitamente en cada incremento del bucle. next() genera una excepción StopIteration cuando no hay más valores para devolver, que se captura implícitamente mediante construcciones en bucle para detener la iteración.

A continuación se muestra un ejemplo sencillo de un contador:

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

Esto imprimirá:

3
4
5
6
7
8

Esto es más fácil de escribir usando un generador, como se explica en una respuesta anterior:

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

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

La salida impresa será la misma.Debajo del capó, el objeto generador admite el protocolo iterador y hace algo más o menos similar a la clase Contador.

El artículo de David Mertz, Iteradores y generadores simples, es una muy buena introducción.

Otros consejos

Hay cuatro formas de construir una función iterativa:

Ejemplos:

# 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

Para ver los cuatro métodos en acción:

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

Lo que resulta en:

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

Nota:

Los dos tipos de generador (uc_gen y uc_genexp) no puede ser reversed();el iterador simple (uc_iter) necesitaría el __reversed__ método mágico (que debe devolver un nuevo iterador que retrocede);y el getitem iterable (uc_getitem) debe tener el __len__ método mágico:

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

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

Para responder a la pregunta secundaria del coronel Panic sobre un iterador infinito evaluado de forma diferida, aquí están esos ejemplos, utilizando cada uno de los cuatro métodos anteriores:

# 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

Lo que da como resultado (al menos para mi ejecución de muestra):

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

En primer lugar el módulo itertools es increíblemente útil para todo tipo de casos en los que un iterador sería útil, pero aquí está todo lo que necesitas para crear un iterador en Python:

producir

¿No es genial?El rendimiento se puede utilizar para reemplazar un normal. devolver en una función.Devuelve el objeto de todos modos, pero en lugar de destruir el estado y salir, guarda el estado para cuando quieras ejecutar la siguiente iteración.A continuación se muestra un ejemplo en acción extraído directamente del lista de funciones de itertools:

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

Como se indica en la descripción de funciones (es el contar() función del módulo itertools...), produce un iterador que devuelve números enteros consecutivos que comienzan con n.

Expresiones generadoras son otra lata de gusanos (¡gusanos increíbles!).Se pueden utilizar en lugar de un Comprensión de listas para ahorrar memoria (las listas por comprensión crean una lista en la memoria que se destruye después de su uso si no se asigna a una variable, pero las expresiones generadoras pueden crear un objeto generador...que es una forma elegante de decir Iterador).A continuación se muestra un ejemplo de una definición de expresión generadora:

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

Esto es muy similar a nuestra definición de iterador anterior, excepto que el rango completo está predeterminado entre 0 y 10.

Acabo de encontrar rango x() (me sorprende no haberlo visto antes...) y lo agregué al ejemplo anterior. rango x() es una versión iterable de rango() lo que tiene la ventaja de no crear previamente la lista.Sería muy útil si tuviera un corpus gigante de datos para iterar y solo tuviera una cantidad limitada de memoria para hacerlo.

Veo a algunos de ustedes haciendo return self en __iter__.Sólo quería señalar que __iter__ puede ser un generador (eliminando así la necesidad de __next__ y levantando StopIteration excepciones)

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

Por supuesto, aquí también se podría crear un generador directamente, pero para clases más complejas puede resultar útil.

Esta pregunta trata sobre objetos iterables, no sobre iteradores.En Python, las secuencias también son iterables, por lo que una forma de crear una clase iterable es hacer que se comporte como una secuencia, es decir.darle __getitem__ y __len__ métodos.Probé esto en Python 2 y 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)

Esta es una función iterable sin yield.Se hace uso de la iter función y un cierre que mantiene su estado en un mutable (list) en el ámbito adjunto para 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)

Para Python 3, el estado de cierre se mantiene inmutable en el ámbito circundante y nonlocal se utiliza en el ámbito local para actualizar la variable de estado.

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)  

Prueba;

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

Todas las respuestas en esta página son realmente excelentes para un objeto complejo.Pero para aquellos que contienen tipos iterables incorporados como atributos, como str, list, set o dict, o cualquier implementación de collections.Iterable, puedes omitir ciertas cosas en tu clase.

class Test(object):
    def __init__(self, string):
        self.string = string

    def __iter__(self):
        # since your string is already iterable
        return (ch for ch in string)

Se puede utilizar como:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e

Si buscas algo breve y sencillo, quizás te baste:

class A(object):
    def __init__(self, l):
        self.data = l

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

ejemplo de uso:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

Inspirado en la respuesta de Matt Gregory, aquí hay un iterador un poco más complicado que devolverá a,b,...,z,aa,ab,...,zz,aaa,aab,...,zzy,zzz.

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

    def __iter__(self):
        return self

    def __next__(self): # Python 3: def __next__(self)
        alpha = ' abcdefghijklmnopqrstuvwxyz'
        n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
        n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
        if n_current > n_high:
            raise StopIteration
        else:
            increment = True
            ret = ''
            for x in self.current[::-1]:
                if 'z' == x:
                    if increment:
                        ret += 'a'
                    else:
                        ret += 'z'
                else:
                    if increment:
                        ret += alpha[alpha.find(x)+1]
                        increment = False
                    else:
                        ret += x
            if increment:
                ret += 'a'
            tmp = self.current
            self.current = ret[::-1]
            return tmp

for c in AlphaCounter('a', 'zzz'):
    print(c)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top