代码之家  ›  专栏  ›  技术社区  ›  saulspatz

Python生成器中的奇怪Bug

  •  1
  • saulspatz  · 技术社区  · 7 年前

    我有一个Knuth算法的实现(“跳舞的链接”),它的行为非常奇怪。我找到了一个解决办法,但这就像魔法一样。下面的脚本测试N queens问题的代码。错误出现在第一个函数中, solve . 参数 limit 应限制生成的解决方案数,默认值0表示“生成所有解决方案”。

    #Test generalized exact cover for n queens problem 
    
    def solve(Cols,  Rows, SecondaryIDs=set(), limit = 0):
        for soln in solver(Cols, Rows, SecondaryIDs):
            print('solve:', limit, soln)
            yield soln
            limit -= 1
            if limit == 0: return
    
    def solver(Cols, Rows, SecondaryIDs, solution=[]):
        live=[col for col in Cols if col not in SecondaryIDs] 
        if not live:
            yield solution
        else:
            col = min(live, key = lambda col: len(Cols[col]))        
            for row in list(Cols[col]):                                          
                solution.append(row)                                            
                columns = select(Cols, Rows, row)                        
                for soln in solver(Cols, Rows, SecondaryIDs, solution):
                    yield soln
                deselect(Cols, Rows, row, columns)
                solution.pop()
    
    def select(Cols, Rows, row):
        columns = []
        for col in Rows[row]:
            for rrow in Cols[col]:
                for ccol in Rows[rrow]:
                    if ccol != col:
                        Cols[ccol].remove(rrow)
            columns.append(Cols.pop(col))  
        return columns
    
    def deselect(Cols, Rows, row, columns): 
        for col in reversed(Rows[row]):
            Cols[col] = columns.pop()
            for rrow in Cols[col]:
                for ccol in Rows[rrow]:
                    if ccol != col:
                        Cols[ccol].add(rrow)
    
    n = 5
    
    # From Dancing Links paper
    solutionCounts = {4:2, 5:10, 6:4, 7:40, 8:92, 9:352, 10:724}
    
    def makeRows(n):
        # There is one row for each cell.    
        rows = dict()
        for rank in range(n):
            for file in range(n):
                rows["R%dF%d"%(rank,file)] = ["R%d"%rank, "F%d"%file, "S%d"%(rank+file), "D%d"%(rank-file)]
        return rows
    
    def makePrimary(n):
        # One primary column for each rank and file
        prim = dict()
        for rank in range(n):
            prim["R%d"%rank] = {"R%dF%d"%(rank,file) for file in range(n)}
        for file in range(n):
            prim["F%d"%file] = {"R%dF%d"%(rank,file) for rank in range(n)}
        return prim
    
    def makeSecondary(n):
        # One secondary column for each diagonal
        second = dict()
        for s in range(2*n-1):
            second["S%d"%s] = {"R%dF%d"%(r, s-r) for r in range(max(0,s-n+1), min(s+1,n))}
        for d in range(-n+1, n):
            second["D%d"%(-d)]={"R%dF%d"%(r, r+d) for r in range(max(0,-d),min(n-d, n))}
        return second
    
    rows = makeRows(n)
    primary = makePrimary(n)
    secondary = makeSecondary(n)
    primary.update(secondary)
    secondary = secondary.keys()
    #for soln in solve(primary, rows, secondary, 15):
        #print(soln)
    solutions = [s for s in solve(primary, rows, secondary)]
    try:
        assert len(solutions) == solutionCounts[n]
    except AssertionError:
        print("actual %d expected %d"%(len(solutions), solutionCounts[n]))
    for soln in solutions:print(soln)
    

    该代码用于生成5皇后问题的前6个解决方案,并且运行良好。(见电话)

    solutions = [s for s in solve(primary, rows, secondary, 6)]
    

    solutions = [s for s in solve(primary, rows, secondary)]
    

    主程序打印10个空列表 [] 作为解决方案,但代码 解决 打印真正的解决方案。如果我超过15,同样的事情也会发生。

    另一件更奇怪的事是,如果我把第13行改成

    yield list(solution)
    

    那么第80行的代码在所有情况下都可以正常工作。我不记得当初写代码的时候,我是怎么偶然发现这个乱七八糟的东西的。我今天看了一下,就变了 yield list(solution) yield solution solution 已经是一个列表。事实上,我试着加上这句话

    assert solution == list(solution)
    

    就在13号线之前,它从来没有升起过 AssertionError

    2 回复  |  直到 7 年前
        1
  •  4
  •   John Kugelman Michael Hodel    7 年前
    yield solution
    

    yield 语句被保留。其中任何一个都可以:

    yield list(solution)
    yield solution[:]
    yield tuple(solution)
    
        2
  •  4
  •   DSM    7 年前

    看到代码前的预测: yield list(solution) 产生浅层 yield solution 生成解决方案列表 ,所以当你在之后改变列表时,你会遇到麻烦。


    看来我是对的。:-)较短版本:

    def weird(solution):
        for i in range(len(solution)):
            yield solution
            solution.pop()
    

    In [8]: result = list(weird(['a','b','c']))
    
    In [9]: result
    Out[9]: [[], [], []]
    

    因为

    In [10]: [id(x) for x in result]
    Out[10]: [140436644005128, 140436644005128, 140436644005128]
    

    但如果我们 产量表(解决方案)

    In [15]: list(less_weird(['a','b','c']))
    Out[15]: [['a', 'b', 'c'], ['a', 'b'], ['a']]
    

    首先,我们看到一个可变的默认参数,这是一个坏主意,但实际上不是您看到的错误的原因:

    def solver(Cols, Rows, SecondaryIDs, solution=[]):
        live=[col for col in Cols if col not in SecondaryIDs] 
        if not live:
            yield solution
    

    在这里你得到了解。。

    else:
        col = min(live, key = lambda col: len(Cols[col]))        
        for row in list(Cols[col]):                                          
            solution.append(row)                                            
            columns = select(Cols, Rows, row)                        
            for soln in solver(Cols, Rows, SecondaryIDs, solution):
                yield soln
            deselect(Cols, Rows, row, columns)
            solution.pop()
    

    你在这里变异了 和你之前的清单一样