Gerador Python vs função de retorno de chamada

Tenho uma classe que resolve um problema exato de cobertura usando um algoritmo recursivo de retorno. Originalmente, implementei a classe com uma função de retorno de chamada que passei para o objeto durante a inicialização. Esse retorno de chamada é chamado sempre que uma solução é encontrada. Ao analisar a implementação de alguém do mesmo problema, vi que eles estavam usando declarações de rendimento para distribuir uma solução; em outras palavras, seu código era um gerador de python. Eu pensei que era uma ideia interessante, então criei uma nova versão da minha classe para usar rendimentos. Em seguida, executei testes de comparação entre as duas versões e, para minha surpresa, achei a versão do gerador 5 vezes mais lenta que a versão de retorno de chamada. Observe que, exceto para alternar um rendimento para um retorno de chamada, o código é idêntic

O que está acontecendo aqui? Estou especulando que, como um gerador precisa salvar informações de estado antes de renderizar e depois restaurar esse estado ao reiniciar na próxima chamada, é esse salvamento / restauração que faz com que a versão do gerador seja muito mais lenta. Se for esse o caso, quanta informação de estado o gerador precisa salvar e restaurar?

Alguma idéia dos especialistas em python?

- Editado 7:40 PDT

Aqui está o código do solucionador que usa yield. Substitua o primeiro rendimento abaixo por uma chamada para a função de retorno de chamada e altere o loop a seguir pelo segundo rendimento para apenas uma chamada recursiva para resolver a versão original desse códig

   def solve(self):
      for tp in self.pieces:
         if self.inuse[tp.name]: continue

         self.inuse[tp.name] = True
         while tp.next_orientation() is not None:
            if tp.insert_piece():
               self.n_trials += 1
               self.pieces_in += 1
               self.free_cells -= tp.size

               if self.pieces_in == len(self.pieces) or self.free_cells == 0:
                  self.solutions += 1
                  self.haveSolution = True
                  yield True
                  self.haveSolution = False
               else:
                  self.table.next_base_square()
                  for tf in self.solve():
                     yield tf

               tp.remove_piece()
               self.pieces_in -= 1
               self.table.set_base_square(tp.base_square)
               self.free_cells += tp.size

         self.inuse[tp.name] = False
         tp.reset_orientation()

O loop de email que chama o solucionador (após a inicialização, é claro) é

   start_time = time.time()
   for tf in s.solve():
      printit(s)

   end_time = time.time()
   delta_time = end_time - start_time

Na versão de retorno de chamada, o loop acaba com apenas uma única chamada a ser resolvid

questionAnswers(1)

yourAnswerToTheQuestion