Para entender o que yieldfaz, 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, stringsarquivos ...
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 mygeneratoruma 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 forusar o gerador.
Agora a parte mais difícil:
A primeira vez que forchamar 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 whilecontinue 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 / Zipsem 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 forloops funcionam .
yieldnão é tão mágico que esta resposta sugere. Quando você chama uma função que contém umayieldinstruçã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 umayieldinstrução, pausa e entrega o objeto. Quando você extrai outro objeto, o Python continua logo após oyielde 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.