Generador de pitón vs función de devolución de llamada

Tengo una clase que resuelve un problema de cobertura exacto utilizando un algoritmo recursivo y de retroceso. Originalmente, implementé la clase con una función de devolución de llamada que pasé al objeto durante la inicialización. Esta devolución de llamada se invoca cada vez que se encuentra una solución. Al observar la implementación de otra persona del mismo problema, vi que estaban usando declaraciones de rendimiento para pasar una solución, en otras palabras, su código era un generador de Python. Pensé que esta era una idea interesante, así que hice una nueva versión de mi clase para usar los rendimientos. Luego realicé pruebas de comparación entre las dos versiones y, para mi sorpresa, encontré que la versión del generador funcionó 5 veces más lento que la versión de devolución de llamada. Tenga en cuenta que, excepto para cambiar el rendimiento de una devolución de llamada, el código es idéntico.

¿Que esta pasando aqui? Estoy especulando que, debido a que un generador necesita guardar la información de estado antes de ceder y luego restaurar ese estado cuando se reinicia en la próxima llamada, es este guardar / restaurar lo que hace que la versión del generador funcione mucho más lentamente. Si este es el caso, ¿cuánta información de estado tiene que guardar y restaurar el generador?

¿Alguna idea de los expertos en python?

- Editado 7:40 PDT

Aquí está el código solucionador que usa el rendimiento. Reemplace el primer rendimiento a continuación con una llamada a la función de devolución de llamada y cambie el siguiente ciclo con el segundo rendimiento a solo una llamada recursiva para resolver la versión original de este código.

   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()

El bucle de correo que invoca al solucionador (después de la inicialización, por supuesto) es

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

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

En la versión de devolución de llamada, el bucle se ha ido con una sola llamada para resolver.

Respuestas a la pregunta(1)

Su respuesta a la pregunta