【问题标题】:Python generator vs callback functionPython生成器与回调函数
【发布时间】:2011-08-07 22:25:30
【问题描述】:

我有一个类使用递归回溯算法解决精确覆盖问题。最初,我使用在初始化期间传递给对象的回调函数来实现该类。每当找到解决方案时都会调用此回调。在查看其他人对同一问题的实现时,我看到他们正在使用 yield 语句来传递解决方案,换句话说,他们的代码是一个 python 生成器。我认为这是一个有趣的想法,所以我制作了一个新版本的课程来使用产量。然后我在两个版本之间进行了比较测试,令我惊讶的是,我发现生成器版本的运行速度比回调版本慢 5 倍。请注意,除了为回调切换 yield 之外,代码是相同的。

这里发生了什么?我推测,因为生成器需要在屈服之前保存状态信息,然后在下一次调用时重新启动时恢复该状态,正是这种保存/恢复使生成器版本运行得这么慢。如果是这种情况,生成器需要保存和恢复多少状态信息?

python 专家有什么想法吗?

--编辑于太平洋夏令时间 7:40

这是使用 yield 的求解器代码。将下面的第一个 yield 替换为对回调函数的调用,并将以下循环与第二个 yield 更改为仅递归调用以解决此代码的原始版本。

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

调用求解器的邮件循环(当然是在初始化之后)是

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

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

在回调版本中,只需一次调用即可解决循环。

【问题讨论】:

  • 请提供一个(简化但完整的)示例以及您如何测量时间。
  • yield来自递归函数听起来需要额外的for 循环才能将结果传递给调用者,不是吗?也许你的意思是协程,用send 传递结果?
  • 通常可以找到多少种解决方案? (您的产量是很多还是相对较少?)
  • 作为记录,我的两个小测试(ideone.com/7XCroideone.com/VuKRnideone.com/DhTJF)似乎表明收益率和回调之间的性能差异不大,回调缓慢改善,而它做更多的工作。我很想看到 OP 的反例。
  • 我为我的代码的生成器版本添加了相关代码。我还展示了 main 如何调用求解器以及我是如何进行计时的。关于 FogleBirds 问题,两个版本都找到了完全相同的一组解决方案,对于给定的问题是正确的。

标签: python profiling generator


【解决方案1】:

我在评论中的意思是,(“从递归函数产生听起来需要额外的 for 循环才能将结果传递给调用者”)是这一行:

          for tf in self.solve():
             yield tf

这些行递归循环来自更深的递归阶段的结果。这意味着在每个递归级别上迭代单个结果,导致很多不必要的循环。

让我用这个例子来说明:

n = 0
def rekurse(z):
    global n
    if z:
        yield z
        for x in rekurse(z-1):
            n += 1
            yield x

print list(rekurse(10))
print n

正如您所看到的,这只是从 10 开始倒计时,因此您期望迭代次数是线性的。不过你可以看到n 以二次方增长——recurse(10) 循环超过 9 个项目,recurse(9) 超过 8 个项目等等。

你拥有的项目越多,Python 在这些简单的行上花费的时间就越多。回调完全避免了这个问题,所以我怀疑这是你的代码的问题。

PEP 380 的优化实现可以解决此问题(请参阅this paragraph)。同时,我认为从递归函数中产生不是一个好主意(至少在它们深度递归的情况下),它们不能很好地协同工作。

【讨论】:

  • 没关系,你是对的。请参阅ideone.com/ylAg2(克隆并调整 N 以查看增长)+1 以启发我们。
  • 我想我明白你在说什么,Jochen。但是,如果我删除循环并仅用递归调用来替换它来解决,它就不再起作用了。这是你期望发生的事情吗?换句话说,为了使生成器版本工作,循环是必需的,但是,因为循环在那里,它相对于代码的回调版本是非常低效的。
  • @delnan:很好,感谢您的分析 :-) @sizzzzlerz:是的,您的代码没有任何错误,您需要 for 循环才能使用 yield在递归函数中。递归函数和 yield 不能很好地协同工作,你最好使用回调或编写没有递归的函数。
猜你喜欢
  • 2016-12-16
  • 2019-10-17
  • 2022-08-16
  • 2013-09-17
  • 2010-12-20
  • 2020-04-23
  • 2011-04-17
相关资源
最近更新 更多