简体   繁体   English

Python生成器与回调函数

[英]Python generator vs callback function

I have a class that solves an exact cover problem using a recursive, backtracking algorithm. 我有一个类使用递归的回溯算法解决一个确切的覆盖问题。 Originally, I implemented the class with a callback function I passed to the object during initialization. 最初,我使用在初始化期间传递给对象的回调函数实现了类。 This callback is invoked whenever a solution is found. 无论何时找到解决方案,都会调用此回调。 In looking at someone else's implementation of the same problem, I saw that they were using yield statements to pass a solution out, in other words, their code was a python generator. 在查看其他人对同一问题的实现时,我看到他们使用yield语句来传递解决方案,换句话说,他们的代码是一个python生成器。 I thought this was an interesting idea so I made a new version of my class to use yields. 我认为这是一个有趣的想法,所以我创建了我的课程的新版本以使用收益率。 I then ran comparison tests between the two versions and, to my surprise, I found the generator version ran 5 times slower than the callback version. 然后,我在两个版本之间运行了比较测试,令我惊讶的是,我发现生成器版本运行速度比回调版本慢5倍。 Note that, except for switching in a yield for a callback, the code is identical. 请注意,除了切换回调的yield之外,代码是相同的。

What is going on here? 这里发生了什么? I'm speculating that, because a generator needs to save state information before yielding and then restore that state when restarting at the next call, it is this save/restore that is what makes the generator version run so much slower. 我猜测,因为生成器需要在屈服之前保存状态信息,然后在下次调用时重新启动时恢复该状态,正是这种保存/恢复使生成器版本运行得慢得多。 If this is the case, how much state information is the generator having to save and restore? 如果是这种情况,发电机必须保存和恢复多少状态信息?

Any ideas from the python experts? 来自python专家的任何想法?

--Edited 7:40 PDT - 编辑7:40太平洋时间

Here is the solver code which uses yield. 这是使用yield的求解器代码。 Replace the first yield below with a call to the callback function and change the following loop with the second yield to just a recursive call to solve for the original version of this code. 通过调用回调函数替换下面的第一个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()

The mail loop which invokes the solver (after initialization, of course) is 调用求解器的邮件循环(当然是在初始化之后)是

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

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

In the callback version, the loop is gone with just a single call to solve. 在回调版本中,只需一次调用就可以解决循环问题。

What i meant in my comment, ("yielding from a recursive function sounds like it requires extra for loops to pass the results down to the caller") is this line: 我在评论中的含义是什么(“从递归函数中产生的声音,它需要额外的循环将结果传递给调用者”)是这一行:

          for tf in self.solve():
             yield tf

These lines recursively loop over the results from the deeper recursion stages. 这些行递归地循环来自更深递归阶段的结果。 That means that a single result is iterated over on each level of the recursion, resulting in a lot of unnecessary looping. 这意味着在递归的每个级别上迭代单个结果,从而导致大量不必要的循环。

Let me illustrate with this example: 让我用这个例子来说明:

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

As you can see this simply counts down from 10, so you'd expect aa linear number of iterations. 正如您所看到的,这只是从10开始倒计时,因此您可以期望线性迭代次数。 What you can see though is that n grows quadratically - recurse(10) loops over 9 items, recurse(9) over 8 items and so on. 你可以看到的是, n二次方式增长 - recurse(10)循环超过9个项目, recurse(9)超过8个项目,依此类推。

The more items you have, the more time Python spends on these simple lines. 你拥有的项目越多,Python花在这些简单线条上的时间就越多。 Callbacks completely avoid that problem, so I'd suspect that is the problem with your code. 回调完全避免了这个问题,所以我怀疑你的代码存在问题。

A optimized implementation of PEP 380 could fix this (see this paragraph ). PEP 380的优化实施可以解决这个问题(参见本段 )。 In the meantime I don't think it's a good idea to yield from recursive functions (at least if they recurse deeply), they just don't work well together. 与此同时,我不认为从递归函数中产生(至少如果它们深度递归),它们只是不能很好地协同工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM