terminar - Python para el comportamiento de iteración y bucle
for i in range python español (6)
Algunos detalles adicionales sobre el comportamiento de iter()
con las clases __getitem__
que carecen de su propio método __iter__
.
Antes de __iter__
había __getitem__
. Si el __getitem__
funciona con int
s desde 0
- len(obj)-1
, entonces iter()
admite estos objetos. Construirá un nuevo iterador que repetidamente llama a __getitem__
con 0
, 1
, 2
, ...
hasta que obtenga un IndexError
, que convierte a StopIteration
.
Consulte esta respuesta para obtener más detalles sobre las diferentes formas de crear un iterador.
Quería entender un poco más acerca de los iterators
, así que por favor corrígeme si estoy equivocado.
Un iterador es un objeto que tiene un puntero al siguiente objeto y se lee como un búfer o secuencia (es decir, una lista vinculada). Son particularmente eficientes porque todo lo que hacen es decirte lo que sigue por referencias en lugar de utilizar la indexación.
Sin embargo, todavía no entiendo por qué está sucediendo el siguiente comportamiento:
In [1]: iter = (i for i in range(5))
In [2]: for _ in iter:
....: print _
....:
0
1
2
3
4
In [3]: for _ in iter:
....: print _
....:
In [4]:
Después de un primer ciclo a través del iterador ( In [2]
) es como si se hubiera consumido y se hubiera dejado vacío, por lo que el segundo ciclo ( In [3]
) no imprime nada.
Sin embargo, nunca le asigné un nuevo valor a la variable iter
.
¿Qué está sucediendo realmente bajo el capó del bucle for
?
Existe un protocolo de iterador en python que define cómo se comportará la instrucción for
con las listas y los dicts, y otras cosas que pueden enlazarse.
Está en los documentos de Python here y here .
La forma en que el protocolo de iterador funciona normalmente tiene la forma de un generador de python. yield
un valor siempre que tengamos un valor hasta que lleguemos al final y luego levantamos StopIteration
Así que vamos a escribir nuestro propio iterador:
def my_iter():
yield 1
yield 2
yield 3
raise StopIteration()
for i in my_iter():
print i
El resultado es:
1
2
3
Un par de cosas para notar sobre eso. El my_iter es una función. my_iter () devuelve un iterador.
Si hubiera escrito usando un iterador como este en su lugar:
j = my_iter() #j is the iterator that my_iter() returns
for i in j:
print i #this loop runs until the iterator is exhausted
for i in j:
print i #the iterator is exhausted so we never reach this line
Y el resultado es el mismo que el anterior. El iter se agota cuando ingresamos al segundo ciclo for.
Pero eso es bastante simplista ¿y algo más complicado? Tal vez tal vez en un bucle ¿por qué no?
def capital_iter(name):
for x in name:
yield x.upper()
raise StopIteration()
for y in capital_iter(''bobert''):
print y
Y cuando se ejecuta, usamos el iterador en el tipo de cadena (que está integrado en iter ). Esto, a su vez, nos permite ejecutar un ciclo de bucle y arrojar los resultados hasta que hayamos terminado.
B
O
B
E
R
T
Así que ahora esto plantea la pregunta, entonces, ¿qué ocurre entre los rendimientos en el iterador?
j = capital_iter("bobert")
print i.next()
print i.next()
print i.next()
print("Hey there!")
print i.next()
print i.next()
print i.next()
print i.next() #Raises StopIteration
La respuesta es que la función está pausada en el rendimiento esperando la siguiente llamada a next ().
B
O
B
Hey There!
E
R
T
Traceback (most recent call last):
File "", line 13, in
StopIteration
For loop básicamente llama al next
método de un objeto que se aplica a ( __next__
en Python 3).
Puedes simular esto simplemente haciendo:
iter = (i for i in range(5))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
print(next(iter))
# this prints 1 2 3 4
En este punto, no hay próximo elemento en el objeto de entrada. Haciendo esto:
print(next(iter))
Se StopIteration
excepción StopIteration
. En este punto, se detendrá. Y el iterador puede ser cualquier objeto que responda a la función next()
y arroje la excepción cuando no haya más elementos. No tiene que ser ningún puntero o referencia (no hay tales cosas en Python de todos modos en sentido C / C ++), lista enlazada, etc.
Su sospecha es correcta: el iterador se ha consumido.
En realidad, su iterador es un generator , que es un objeto que tiene la capacidad de repetirse solo una vez.
type((i for i in range(5))) # says it''s type generator
def another_generator():
yield 1 # the yield expression makes it a generator, not a function
type(another_generator()) # also a generator
La razón por la cual son eficientes no tiene nada que ver con decirte lo que sigue "por referencia". Son eficientes porque solo generan el siguiente artículo a pedido; todos los artículos no se generan a la vez. De hecho, puedes tener un generador infinito:
def my_gen():
while True:
yield 1 # again: yield means it is a generator, not a function
for _ in my_gen(): print(_) # hit ctl+c to stop this infinite loop!
Algunas otras correcciones para ayudar a mejorar su comprensión:
- El generador no es un puntero y no se comporta como un puntero, como es posible que esté familiarizado en otros idiomas.
- Una de las diferencias con respecto a otros lenguajes: como se dijo anteriormente, cada resultado del generador se genera sobre la marcha. El siguiente resultado no se produce hasta que se solicite.
- La combinación de palabras clave
for
in
acepta un objeto iterable como su segundo argumento. - El objeto iterable puede ser un generador, como en su caso de ejemplo, pero también puede ser cualquier otro objeto iterable, como una
list
odict
, o un objetostr
(cadena), o un tipo definido por el usuario que proporcione el funcionalidad. - La función
iter
se aplica al objeto para obtener un iterador (por cierto, no utilicesiter
como nombre de variable en Python, como lo has hecho, es una de las palabras clave). En realidad, para ser más precisos, se__iter__
método__iter__
del objeto (que es, en su mayor parte, toda la funcióniter
hace de todos modos;__iter__
es uno de los llamados "métodos mágicos" de Python). - Si la llamada a
__iter__
es exitosa, la funciónnext()
se aplica al objeto iterable una y otra vez, en un bucle, y la primera variable proporcionada parafor
se asigna al resultado de la funciónnext()
. (Recuerde: el objeto iterable podría ser un generador, o un iterador de un objeto contenedor, o cualquier otro objeto iterable). En realidad, para ser más precisos: llama al método__next__
del objeto iterador, que es otro "método mágico". - El bucle
for
finaliza cuandonext()
genera la excepciónStopIteration
(que generalmente ocurre cuando iterable no tiene otro objeto que ceder cuando se llama anext()
).
Puede implementar manualmente un bucle for
en python de esta manera (probablemente no perfecto, pero lo suficientemente cerca):
try:
temp = iterable.__iter__()
except AttributeError():
raise TypeError("''{}'' object is not iterable".format(type(iterable).__name__))
else:
while True:
try:
_ = temp.__next__()
except StopIteration:
break
except AttributeError:
raise TypeError("iter() returned non-iterator of type ''{}''".format(type(temp).__name__))
# this is the "body" of the for loop
continue
No hay diferencia entre el código de ejemplo anterior y el anterior.
En realidad, la parte más interesante de un bucle for
no es for
, sino para in
. Usar in
en sí mismo produce un efecto diferente que for
in
, pero es muy útil para entender qué hace con sus argumentos, ya que for
implementa un comportamiento muy similar.
Cuando se usa por sí mismo, la palabra clave
in
invoca primero el método__contains__
del objeto, que es otro "método mágico" (tenga en cuenta que este paso se omite al usarfor
in
). Usandoin
sí mismo en un contenedor, puede hacer cosas como esta:1 in [1, 2, 3] # True ''He'' in ''Hello'' # True 3 in range(10) # True ''eH'' in ''Hello''[::-1] # True
Si el objeto iterable NO es un contenedor (es decir, no tiene un método
__contains__
),in
siguientes intentos llama al método__iter__
del objeto. Como se dijo anteriormente: el método__iter__
devuelve lo que se conoce en Python como un iterator . Básicamente, un iterador es un objeto que puede usar la función genérica incorporadanext()
en 1 . Un generador es solo un tipo de iterador.- Si la llamada a
__iter__
es exitosa, la palabra clavein
aplica la funciónnext()
al objeto iterable una y otra vez. (Recuerde: el objeto iterable podría ser un generador, un iterador de un objeto contenedor o cualquier otro objeto iterable). En realidad, para ser más precisos: llama al método__next__
del objeto iterador). - Si el objeto no tiene un método
__iter__
para devolver un iterador, entonces vuelve al protocolo de iteración antiguo utilizando el método__getitem__
del objeto 2 . - Si todos los intentos anteriores fallan, obtendrá una excepción
TypeError
.
Si desea crear su propio tipo de objeto para iterar (es decir, puede usarlo for
, o simplemente, sobre él), es útil conocer la palabra clave yield
, que se usa en generators (como se mencionó anteriormente).
class MyIterable():
def __iter__(self):
yield 1
m = MyIterable()
for _ in m: print(_) # 1
1 in m # True
La presencia de yield
convierte una función o método en un generador en lugar de una función / método regular. No necesita el método __next__
si usa un generador (trae __next__
junto con él automáticamente).
Si desea crear su propio tipo de objeto contenedor (es decir, puede usarlo solo, pero NO for
), solo necesita el método __contains__
.
class MyUselessContainer():
def __contains__(self, obj):
return True
m = MyUselessContainer()
1 in m # True
''Foo'' in m # True
TypeError in m # True
None in m # True
1 Tenga en cuenta que, para ser un iterador, un objeto debe implementar iterator . Esto solo significa que los métodos __next__
y __iter__
deben implementarse correctamente (los generadores vienen con esta funcionalidad "gratis", por lo que no tiene que preocuparse al usarlos). También tenga en cuenta que el método ___next__
es el next
(sin guiones bajos) en Python 2 .
2 Consulte esta respuesta para conocer las diferentes formas de crear clases iterables.
Concepto 1
Todos los generadores son iteradores pero todos los iteradores no son generadores
Concepto 2
Un iterador es un objeto con un método siguiente (Python 2) o siguiente (Python 3).
Concepto 3
Citar desde wiki generator funciones de los generadores le permiten declarar una función que se comporta como un iterador, es decir, puede usarse en un ciclo for.
En tu caso
>>> it = (i for i in range(5))
>>> type(it)
<type ''generator''>
>>> callable(getattr(it, ''iter'', None))
False
>>> callable(getattr(it, ''next'', None))
True
Extracto del libro de práctica de Python :
5. Iteradores y generadores
5.1. Iteradores
Usamos como enunciado para pasar por encima de una lista.
>>> for i in [1, 2, 3, 4]:
... print i,
...
1
2
3
4
Si lo usamos con una cadena, pasa por encima de sus caracteres.
>>> for c in "python":
... print c
...
p
y
t
h
o
n
Si lo usamos con un diccionario, pasa por encima de sus teclas.
>>> for k in {"x": 1, "y": 2}:
... print k
...
y
x
Si lo usamos con un archivo, pasa por encima de las líneas del archivo.
>>> for line in open("a.txt"):
... print line,
...
first line
second line
Entonces, hay muchos tipos de objetos que se pueden usar con un ciclo for. Estos se llaman objetos iterables.
Hay muchas funciones que consumen estos iterables.
>>> ",".join(["a", "b", "c"])
''a,b,c''
>>> ",".join({"x": 1, "y": 2})
''y,x''
>>> list("python")
[''p'', ''y'', ''t'', ''h'', ''o'', ''n'']
>>> list({"x": 1, "y": 2})
[''y'', ''x'']
5.1.1. El protocolo de iteración
La función incorporada iter toma un objeto iterable y devuelve un iterador.
>>> x = iter([1, 2, 3])
>>> x
<listiterator object at 0x1004ca850>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Cada vez que llamamos al siguiente método en el iterador, nos da el siguiente elemento. Si no hay más elementos, se levanta una StopIteration.
Los iteradores se implementan como clases. Aquí hay un iterador que funciona como una función incorporada de xrange.
class yrange:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
El método iter es lo que hace que un objeto sea iterable. Detrás de escena, la función iter llama al método iter en el objeto dado.
El valor de retorno de iter es un iterador. Debería tener un siguiente método y elevar StopIteration cuando no hay más elementos.
Vamos a probarlo:
>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 14, in next
StopIteration
Muchas funciones incorporadas aceptan iteradores como argumentos.
>>> list(yrange(5))
[0, 1, 2, 3, 4]
>>> sum(yrange(5))
10
En el caso anterior, tanto el iterable como el iterador son el mismo objeto. Tenga en cuenta que el método iter se devolvió a sí mismo. No es necesario que sea el caso siempre.
class zrange:
def __init__(self, n):
self.n = n
def __iter__(self):
return zrange_iter(self.n)
class zrange_iter:
def __init__(self, n):
self.i = 0
self.n = n
def __iter__(self):
# Iterators are iterables too.
# Adding this functions to make them so.
return self
def next(self):
if self.i < self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
Si tanto iteratable como iterator son el mismo objeto, se consume en una sola iteración.
>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]
5.2. Generadores
Los generadores simplifican la creación de iteradores. Un generador es una función que produce una secuencia de resultados en lugar de un solo valor.
def yrange(n):
i = 0
while i < n:
yield i
i += 1
Cada vez que se ejecuta la instrucción de rendimiento, la función genera un nuevo valor.
>>> y = yrange(3)
>>> y
<generator object yrange at 0x401f30>
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Entonces un generador también es un iterador. No tiene que preocuparse por el protocolo del iterador.
La palabra "generador" se usa confusamente para referirse tanto a la función que genera como a lo que genera. En este capítulo, usaré la palabra "generador" para referirme al objeto generado y "función del generador" para referirme a la función que lo genera.
¿Puedes pensar en cómo funciona internamente?
Cuando se llama a una función de generador, devuelve un objeto generador sin siquiera comenzar la ejecución de la función. Cuando se llama al siguiente método por primera vez, la función comienza a ejecutarse hasta que alcanza la declaración de rendimiento. El valor obtenido es devuelto por la próxima llamada.
El siguiente ejemplo demuestra la interacción entre el rendimiento y la llamada al siguiente método en el objeto generador.
>>> def foo():
... print "begin"
... for i in range(3):
... print "before yield", i
... yield i
... print "after yield", i
... print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0
0
>>> f.next()
after yield 0
before yield 1
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Veamos un ejemplo:
def integers():
"""Infinite sequence of integers."""
i = 1
while True:
yield i
i = i + 1
def squares():
for i in integers():
yield i * i
def take(n, seq):
"""Returns first n values from the given sequence."""
seq = iter(seq)
result = []
try:
for i in range(n):
result.append(seq.next())
except StopIteration:
pass
return result
print take(5, squares()) # prints [1, 4, 9, 16, 25]