python - the - ¿Cómo se captura la excepción StopIteration de rendimiento?
python__ iter__ method (4)
Por qué en el ejemplo termina la función:
def func(iterable):
while True:
val = next(iterable)
yield val
¿Pero si desactivo la función de declaración de rendimiento, se generará la excepción StopIteration?
EDITAR: Lo siento por engañar a los chicos. Sé qué son los generadores y cómo usarlos. Por supuesto, cuando dije que la función terminaba, no me refería a una evaluación ansiosa de la función. Solo implicé que cuando uso la función para producir el generador:
gen = func(iterable)
en caso de func funciona y devuelve el mismo generador, pero en caso de func2:
def func2(iterable):
while True:
val = next(iterable)
eleva StopIteration en lugar de Ninguno retorno o bucle infinito.
Déjame ser más específico. Hay una función de tee en itertools que es equivalente a:
def tee(iterable, n=2):
it = iter(iterable)
deques = [collections.deque() for i in range(n)]
def gen(mydeque):
while True:
if not mydeque: # when the local deque is empty
newval = next(it) # fetch a new value and
for d in deques: # load it to all the deques
d.append(newval)
yield mydeque.popleft()
return tuple(gen(d) for d in deques)
Hay, de hecho, algo de magia, porque la función anidada gen tiene un bucle infinito sin declaraciones de ruptura. La función gen finaliza debido a una excepción StopIteration cuando no hay elementos en ella . Pero termina correctamente (sin generar excepciones), es decir, simplemente detiene el bucle. Entonces la pregunta es : ¿dónde se maneja StopIteration ?
Cuando una función contiene yield
, la llamada no ejecuta realmente nada, simplemente crea un objeto generador. Solo la iteración sobre este objeto ejecutará el código. Entonces, supongo que solo estás llamando a la función, lo que significa que la función no StopIteration
porque nunca se está ejecutando.
Dada tu función, y una iterable:
def func(iterable):
while True:
val = next(iterable)
yield val
iterable = iter([1, 2, 3])
Esta es la forma incorrecta de llamarlo:
func(iterable)
Esta es la manera correcta:
for item in func(iterable):
# do something with item
También puede almacenar el generador en una variable y llamar a next()
en él (o iterar sobre él de alguna otra manera):
gen = func(iterable)
print(next(gen)) # prints 1
print(next(gen)) # prints 2
print(next(gen)) # prints 3
print(next(gen)) # StopIteration
Por cierto, una mejor manera de escribir su función es la siguiente:
def func(iterable):
for item in iterable:
yield item
O en Python 3.3 y posteriores:
def func(iterable):
yield from iter(iterable)
Por supuesto, los generadores reales rara vez son tan triviales. :-)
Para responder a su pregunta acerca de dónde se StopIteration
el StopIteration
en el generador de gen
creado dentro de itertools.tee
: no lo hace. Corresponde al consumidor de los resultados del tee
para capturar la excepción a medida que se repiten.
En primer lugar, es importante tener en cuenta que una función generadora (que es cualquier función con una declaración de yield
en ella, en cualquier lugar) es fundamentalmente diferente de una función normal. En lugar de ejecutar el código de la función cuando se le llama, en cambio, solo obtendrá un objeto generator
cuando llame a la función. Solo cuando recorra el generador ejecutará el código.
Una función de generador nunca terminará de iterar sin elevar StopIteration
(a menos que en su lugar StopIteration
alguna otra excepción). StopIteration
es la señal del generador de que está hecho y no es opcional. Si llega a una declaración de return
o al final del código de la función del generador sin elevar nada, Python aumentará StopIteration
por usted.
Esto es diferente de las funciones regulares, que devuelven None
si llegan al final sin devolver nada más. Se relaciona con las diferentes formas en que funcionan los generadores, como describí anteriormente.
Aquí hay una función de generador de ejemplo que hará que sea fácil ver cómo se StopIteration
:
def simple_generator():
yield "foo"
yield "bar"
# StopIteration will be raised here automatically
Esto es lo que pasa cuando lo consumes:
>>> g = simple_generator()
>>> next(g)
''foo''
>>> next(g)
''bar''
>>> next(g)
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
next(g)
StopIteration
Llamar a simple_generator
siempre devuelve un objeto generator
inmediatamente (sin ejecutar ningún código en la función). Cada llamada de next
en el objeto generador ejecuta el código hasta la siguiente declaración de yield
y devuelve el valor producido. Si no hay más que obtener, se StopIteration
.
Ahora, normalmente no ve excepciones de StopIteration
. La razón de esto es que usualmente consumes generadores dentro for
bucles. Una instrucción for
llamará automáticamente la next
una y otra vez hasta que se StopIteration
. StopIteration
y suprimirá la excepción StopIteration
para usted, por lo que no necesita StopIteration
con los bloques try
/ except
para lidiar con ella.
Un bucle for
como for item in iterable: do_suff(item)
es casi exactamente equivalente a este bucle while (la única diferencia es que un real for
no necesita una variable temporal para mantener el iterador):
iterator = iter(iterable)
try:
while True:
item = next(iterator)
do_stuff(item)
except StopIteration:
pass
finally:
del iterator
La función de generador de gen
que mostró en la parte superior es una excepción. Utiliza la excepción StopIteration
producida por el iterador que consume, ya que es su propia señal de que se ha terminado la iteración. Es decir, en lugar de capturar el StopIteration
y luego salir del bucle, simplemente deja la excepción sin ser detectada (presumiblemente para ser atrapada por algún código de nivel superior).
Sin relación con la pregunta principal, hay otra cosa que quiero señalar. En tu código, estás llamando a next
a una variable llamada iterable
. Si toma ese nombre como documentación para el tipo de objeto que obtendrá, esto no es necesariamente seguro.
next
es parte del protocolo del iterator
, no el protocolo iterable
(o container
). Puede funcionar para algunos tipos de iterables (como archivos y generadores, ya que esos tipos son sus propios iteradores), pero fallará para otros iterables, como tuplas y listas. El enfoque más correcto es invocar iter
en su valor iterable
, luego llamar al next
iterador que recibe. (O simplemente úselo for
bucles, que llaman a ambos iter
y al next
para usted en el momento apropiado)
Edición: acabo de encontrar mi propia respuesta en una búsqueda de Google para una pregunta relacionada, y pensé que me actualizaría para señalar que la respuesta anterior no será completamente cierta en futuras versiones de Python. La PEP 479 está cometiendo un error al permitir que una StopIteration
ser StopIteration
desde una función de generador. Si eso sucede, Python lo convertirá en una excepción RuntimeError
.
Esto significa que el código como los ejemplos en itertools
que usan una función StopIteration
para interrumpir la función de un generador deberá modificarse. Por lo general, deberá capturar la excepción con un try
/ except
y luego return
.
Debido a que este es un cambio incompatible hacia atrás, se está introduciendo gradualmente. En Python 3.5, todo el código funcionará como antes por defecto, pero puede obtener el nuevo comportamiento con from __future__ import generator_stop
. En Python 3.6, el código seguirá funcionando, pero dará una advertencia. En Python 3.7, el nuevo comportamiento se aplicará todo el tiempo.
Sin el yield
, recorres todo el iterable
sin parar de hacer nada con val
. El bucle while no captura la excepción StopIteration
. Un equivalente for
bucle sería:
def func(iterable):
for val in iterable:
pass
que captura el StopIteration
y simplemente sale del bucle y, por lo tanto, regresa de la función.
Puedes capturar explícitamente la excepción:
def func(iterable):
while True:
try:
val = next(iterable)
except StopIteration:
break
yield
no atrapa el StopIteration
. Lo que el yield
hace para su función es que hace que se convierta en una función generadora en lugar de una función regular. Por lo tanto, el objeto devuelto de la llamada a la función es un objeto iterable (que calcula el siguiente valor cuando se lo pide con la next
función (a la que se llama implícitamente mediante un bucle for)). Si deja la declaración de yield
fuera de ella, entonces Python ejecuta todo el bucle while de inmediato, lo que termina agotando el iterable (si es finito) y eleva el StopIteration
justo cuando lo llama.
considerar:
x = func(x for x in [])
next(x) #raises StopIteration
Un bucle for
captura la excepción: así es como sabe cuándo dejar de llamar next
de la iteración que le dio.