Para que serve yield no Python?

Estou aprendendo a programar e estava lendo uns códigos, mas não entendi para que serve yield no Python. Alguém poderia me ajudar?

Exemplos de código:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild 
result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

O que acontece quando o método _get_child_candidates é chamado? Uma lista é retornada? Um único elemento? É chamado de novo? Quando as chamadas subsequentes serão interrompidas?

Para entender para que serve yield no Python, primeiro vou explicar o que são os generators, já que vai precisar de um para entender o outro.

O que são geradores?

Geradores são um tipo de iterável que você só pode iterar uma vez. Os geradores não armazenam todos os valores na memória, eles geram os valores na hora. Por exemplo:

mygenerator = (x*x for x in range(3))
for i in mygenerator:
  print(i)

# 0
# 1
# 4

É exatamente o mesmo, exceto que você usou () em vez de []. Mas você não pode executar for i in mygenerator uma segunda vez, pois os geradores só podem ser usados uma vez: eles calculam 0, depois esquecem e calculam 1, e terminam de calcular 4, um por um.

Para que serve yield?

yield é uma palavra-chave que é usada como return, exceto que a função retornará um gerador. Esta função abaixo retornará um enorme conjunto de valores que você só precisará ler uma vez:

def create_generator():
   mylist = range(3)
   for i in mylist:
      yield i*i
mygenerator = create_generator() # create a generator
print(mygenerator) # mygenerator is an object!
# <generator object create_generator at 0xb7555c34>
for i in mygenerator:
   print(i)

#0
#1
#4

Quando você usa yield, o código que você escreveu no corpo da função não é executado. A função só retorna o objeto gerador. Então, seu código continuará de onde parou cada vez que for usar o gerador.

A primeira vez que for chama o objeto generator criado a partir de sua função, ele executará o código em sua função desde o início até atingir yield, então 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 atingir o yield. Isso pode ser porque o loop chegou ao fim ou porque você não satisfaz mais um if/else.

A dúvida sobe o seu código

  • 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. Neste caso, candidate.extend(node._get_child_candidates(distance, min_dist, max_dist)) esgota todos os valores do gerador, mas continua criando novos objetos geradores que produzirão valores diferentes dos anteriores, pois não é aplicado no mesmo nó.

  • O método extend() é um método de objeto de lista que espera um iterável e adiciona seus valores à lista. Só que no caso deste código, é um gerador, então é bom porque não é preciso ler o mesmo valor duas vezes e, se você tiver um número muito grande de dados, eles não ficam salvos na sua memória.