Para entender o que yield
faz, você deve entender o que são geradores . E antes que você possa entender geradores, você deve entender iterables .
Iterables
Ao criar uma lista, você pode ler seus itens um por um. A leitura de seus itens um por um é chamada iteração:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
é um iterável . Ao usar uma compreensão de lista, você cria uma lista e, portanto, é iterável:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Tudo o que você pode usar " for... in...
" é iterável; lists
, strings
arquivos ...
Essas iteráveis são úteis porque você pode lê-las quantas vezes quiser, mas armazena todos os valores na memória e isso nem sempre é o que você deseja quando possui muitos valores.
Geradores
Geradores são iteradores, um tipo de iterável que você pode repetir apenas uma vez . Os geradores não armazenam todos os valores na memória, eles geram os valores rapidamente :
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
É o mesmo, exceto que você usou em ()
vez de []
. MAS, você não pode executar for i in mygenerator
uma segunda vez, uma vez que os geradores podem ser usados apenas uma vez: eles calculam 0, esquecem-no e calculam 1 e terminam o cálculo 4, um por um.
Produção
yield
é uma palavra-chave usada como return
, exceto que a função retornará um gerador.
>>> 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
Aqui está um exemplo inútil, mas é útil quando você sabe que sua função retornará um enorme conjunto de valores que você precisará ler apenas uma vez.
Para dominar yield
, você deve entender que, quando você chama a função, o código que você escreveu no corpo da função não é executado. A função retorna apenas o objeto gerador, isso é um pouco complicado :-)
Em seguida, seu código continuará de onde parou sempre que for
usar o gerador.
Agora a parte mais difícil:
A primeira vez que for
chamar o objeto gerador criado a partir da sua função, ele executará o código na sua função desde o início até que acerte yield
, e retornará o primeiro valor do loop. Em seguida, cada chamada subsequente executará outra iteração do loop que você escreveu na função e retornará o próximo valor. Isso continuará até que o gerador seja considerado vazio, o que acontece quando a função é executada sem bater yield
. Isso pode ser porque o loop chegou ao fim ou porque você não satisfaz mais um "if/else"
.
Seu código explicado
Gerador:
# 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
Chamador:
# 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 contém várias partes inteligentes:
O loop itera em uma lista, mas a lista se expande enquanto o loop está sendo iterado :-) É uma maneira concisa de passar por todos esses dados aninhados, mesmo que seja um pouco perigoso, pois você pode acabar com um loop infinito. Nesse caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
esgote todos os valores do gerador, mas while
continue criando novos objetos geradores que produzirão valores diferentes dos anteriores, pois não são aplicados no mesmo nó.
O extend()
método é um método de objeto de lista que espera uma iterável e adiciona seus valores à lista.
Geralmente passamos uma lista para ele:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Mas no seu código, ele obtém um gerador, o que é bom porque:
- Você não precisa ler os valores duas vezes.
- Você pode ter muitos filhos e não os quer armazenados na memória.
E funciona porque o Python não se importa se o argumento de um método é uma lista ou não. O Python espera iterables, para que ele funcione com strings, listas, tuplas e geradores! Isso é chamado de digitação de pato e é uma das razões pelas quais o Python é tão legal. Mas isso é outra história, para outra pergunta ...
Você pode parar por aqui ou ler um pouco para ver um uso avançado de um gerador:
Controlando a exaustão do gerador
>>> 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__())
ouprint(next(corner_street_atm))
Pode ser útil para várias coisas, como controlar o acesso a um recurso.
Itertools, seu melhor amigo
O módulo itertools contém funções especiais para manipular iteráveis. Já desejou duplicar um gerador? Cadeia de dois geradores? Agrupar valores em uma lista aninhada com uma linha única? Map / Zip
sem criar outra lista?
Então apenas import itertools
.
Um exemplo? Vamos ver as possíveis ordens de chegada para uma corrida de quatro cavalos:
>>> 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)]
Entendendo os mecanismos internos da iteração
A iteração é um processo que implica iteráveis (implementando o __iter__()
método) e iteradores (implementando o __next__()
método). Iteráveis são quaisquer objetos dos quais você pode obter um iterador. Iteradores são objetos que permitem iterar em iterables.
Há mais sobre isso neste artigo sobre como os for
loops funcionam .
yield
não é tão mágico que esta resposta sugere. Quando você chama uma função que contém umayield
instrução em qualquer lugar, obtém um objeto gerador, mas nenhum código é executado. Então, toda vez que você extrai um objeto do gerador, o Python executa o código na função até chegar a umayield
instrução, pausa e entrega o objeto. Quando você extrai outro objeto, o Python continua logo após oyield
e continua até chegar a outroyield
(geralmente o mesmo, mas uma iteração posteriormente). Isso continua até que a função termine no final, momento em que o gerador é considerado esgotado.