Para entender lo que yield
hace, debes entender qué son los generadores . Y antes de que pueda comprender los generadores, debe comprender los iterables .
Iterables
Cuando crea una lista, puede leer sus elementos uno por uno. Leer sus elementos uno por uno se llama iteración:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
Es un iterable . Cuando usas una comprensión de lista, creas una lista, y así un iterable:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Todo lo que puedes usar " for... in...
" es iterable; lists
, strings
archivos ...
Estos iterables son útiles porque puede leerlos todo lo que desee, pero almacena todos los valores en la memoria y esto no siempre es lo que desea cuando tiene muchos valores.
Generadores
Los generadores son iteradores, un tipo de iterativo que solo puede iterar una vez . Los generadores no almacenan todos los valores en la memoria, generan los valores sobre la marcha :
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
Es igual, excepto que usaste en ()
lugar de []
. PERO, no puede realizar for i in mygenerator
una segunda vez ya que los generadores solo se pueden usar una vez: calculan 0, luego se olvidan y calculan 1, y terminan de calcular 4, uno por uno.
rendimiento
yield
es una palabra clave que se usa como return
, excepto que la función devolverá un generador.
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Aquí es un ejemplo inútil, pero es útil cuando sabes que tu función devolverá un gran conjunto de valores que solo necesitarás leer una vez.
Para dominar yield
, debe comprender que cuando llama a la función, el código que ha escrito en el cuerpo de la función no se ejecuta. La función solo devuelve el objeto generador, esto es un poco complicado :-)
Luego, su código continuará desde donde lo dejó cada vez que for
use el generador.
Ahora la parte difícil:
La primera vez que for
llama al objeto generador creado a partir de su función, ejecutará el código en su función desde el principio hasta que llegue yield
, luego devolverá el primer valor del bucle. Luego, cada llamada posterior ejecutará otra iteración del bucle que ha escrito en la función y devolverá el siguiente valor. Esto continuará hasta que el generador se considere vacío, lo que sucede cuando la función se ejecuta sin presionar yield
. Eso puede ser porque el ciclo ha llegado a su fin, o porque ya no satisfaces un "if/else"
.
Tu código explicado
Generador:
# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):
# Here is the code that will be called each time you use the generator object:
# If there is still a child of the node object on its left
# AND if the distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# If there is still a child of the node object on its right
# AND if the distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# If the function arrives here, the generator will be considered empty
# there is no more than two values: the left and the right children
Llamador:
# Create an empty list and a list with the current object reference
result, candidates = list(), [self]
# Loop on candidates (they contain only one element at the beginning)
while candidates:
# Get the last candidate and remove it from the list
node = candidates.pop()
# Get the distance between obj and the candidate
distance = node._get_dist(obj)
# If distance is ok, then you can fill the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate in the candidate's list
# so the loop will keep running until it will have looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
Este código contiene varias partes inteligentes:
El ciclo itera en una lista, pero la lista se expande mientras se repite el ciclo :-) Es una forma concisa de revisar todos estos datos anidados, incluso si es un poco peligroso, ya que puede terminar con un ciclo infinito. En este caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
agote todos los valores del generador, pero while
sigue creando nuevos objetos generadores que producirán valores diferentes de los anteriores ya que no se aplica en el mismo nodo.
El extend()
método es un método de objeto de lista que espera un iterable y agrega sus valores a la lista.
Por lo general, le pasamos una lista:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Pero en su código, obtiene un generador, lo cual es bueno porque:
- No necesita leer los valores dos veces.
- Es posible que tenga muchos hijos y no quiera que todos estén almacenados en la memoria.
Y funciona porque a Python no le importa si el argumento de un método es una lista o no. ¡Python espera iterables para que funcione con cadenas, listas, tuplas y generadores! Esto se llama escribir pato y es una de las razones por las que Python es tan genial. Pero esta es otra historia, para otra pregunta ...
Puede detenerse aquí o leer un poco para ver un uso avanzado de un generador:
Controlando el agotamiento de un generador
>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
Nota: Para Python 3, use print(corner_street_atm.__next__())
oprint(next(corner_street_atm))
Puede ser útil para varias cosas como controlar el acceso a un recurso.
Itertools, tu mejor amigo
El módulo itertools contiene funciones especiales para manipular iterables. ¿Alguna vez quisiste duplicar un generador? ¿Cadena de dos generadores? ¿Agrupar valores en una lista anidada con una línea? Map / Zip
sin crear otra lista?
Entonces solo import itertools
.
¿Un ejemplo? Veamos las posibles órdenes de llegada para una carrera de cuatro caballos:
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]
Comprender los mecanismos internos de iteración
La iteración es un proceso que implica iterables (implementando el __iter__()
método) e iteradores (implementando el __next__()
método). Iterables son los objetos de los que puede obtener un iterador. Los iteradores son objetos que le permiten iterar en iterables.
Hay más sobre esto en este artículo sobre cómo for
funcionan los bucles .
yield
esta respuesta no es tan mágica como sugiere. Cuando llama a una función que contiene unayield
declaración en cualquier lugar, obtiene un objeto generador, pero no se ejecuta ningún código. Luego, cada vez que extrae un objeto del generador, Python ejecuta código en la función hasta que se trata de unayield
declaración, luego hace una pausa y entrega el objeto. Cuando extrae otro objeto, Python se reanuda justo despuésyield
y continúa hasta que alcanza otroyield
(a menudo el mismo, pero una iteración más adelante). Esto continúa hasta que la función se ejecuta al final, en cuyo punto el generador se considera agotado.