【问题标题】:Confounding recursive list append in PythonPython中令人困惑的递归列表追加
【发布时间】:2013-11-22 22:50:30
【问题描述】:

我正在尝试创建一对函数,给定一个“起始”数字列表,它将递归地添加到每个索引位置,直到定义的最大值(与里程表在汽车中的工作方式非常相似-- 每个计数轮增加至 9,然后重置为 1 并转移到下一个轮上)。

代码如下:

number_list = []

def counter(start, i, max_count):
    if start[len(start)-1-i] < max_count:
        start[len(start)-1-i] += 1
        return(start, i, max_count)
    else:
        for j in range (len(start)):
            if start[len(start)-1-i-j] == max_count:
                start[len(start)-1-i-j] = 1
            else:
                start[len(start)-1-i-j] += 1
                return(start, i, max_count)

def all_values(fresh_start, i, max_count):
    number_list.append(fresh_start)
    new_values = counter(fresh_start,i,max_count)
    if new_values != None:
        all_values(*new_values) 

但是,当我运行 all_values([1,1,1],0,3) 并打印 number_list 时,我得到:

[[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1],    
[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], 
[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], 
[1, 1, 1], [1, 1, 1], [1, 1, 1]]

这是不幸的。加倍知道如果我将 all_values 的第一行替换为

print(fresh_start)

我得到了我所追求的:

[1, 1, 1]
[1, 1, 2]
[1, 1, 3]
[1, 2, 1]
[1, 2, 2]
[1, 2, 3]
[1, 3, 1]
[1, 3, 2]
[1, 3, 3]
[2, 1, 1]
[2, 1, 2]
[2, 1, 3]
[2, 2, 1]
[2, 2, 2]
[2, 2, 3]
[2, 3, 1]
[2, 3, 2]
[2, 3, 3]
[3, 1, 1]
[3, 1, 2]
[3, 1, 3]
[3, 2, 1]
[3, 2, 2]
[3, 2, 3]
[3, 3, 1]
[3, 3, 2]
[3, 3, 3]

我已经尝试制作 fresh_start 的副本(通过 temp = fresh_start)并附加它,但输出没有变化。

谁能提供任何关于我可以做些什么来修复我的代码的见解?关于如何简化问题的反馈也将受到欢迎。

非常感谢!

【问题讨论】:

    标签: python list function recursion


    【解决方案1】:
    temp = fresh_start
    

    不制作副本。追加不会复制,分配不会复制,几乎任何没有说它复制的东西都不会复制。如果您想要一份副本,请对其进行切片:

    fresh_start[:]
    

    是一个副本。

    【讨论】:

      【解决方案2】:

      在 Python 解释器中尝试以下操作:

      >>> a = [1,1,1]
      >>> b = []
      >>> b.append(a)
      >>> b.append(a)
      >>> b.append(a)
      >>> b
      [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
      >>> b[2][2] = 2
      >>> b
      [[1, 1, 2], [1, 1, 2], [1, 1, 2]]
      

      这是您代码中发生的事情的简化版本。但是为什么会这样呢?

      b.append(a) 实际上并没有复制a 并将其填充到b 的数组中。它正在对a 进行引用。它就像网络浏览器中的书签:当您使用书签打开网页时,您希望看到的网页是现在的样子,而不是您添加书签时的样子。但这也意味着,如果您在同一页面上有多个书签,并且该页面发生更改,那么无论您关注哪个书签,您都会看到更改后的版本。

      temp = aa = [1,1,1] 的情况相同。 tempa 是恰好包含三个数组的特定数组的“书签”。而在上面的例子中,b 是一个数组的书签...它包含三个书签,指向同一个数组,其中包含三个书签。

      所以你要做的是创建一个新数组并复制旧数组的元素。最快的方法是获取包含整个数组的数组切片,如 user2357112 所示:

      >>> a = [1,1,1]
      >>> b = []
      >>> b.append(a[:])
      >>> b.append(a[:])
      >>> b.append(a[:])
      >>> b
      [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
      >>> b[2][2] = 2
      >>> b
      [[1, 1, 1], [1, 1, 1], [1, 1, 2]]
      

      好多了。

      【讨论】:

      • 非常感谢您的解释——以这种方式来看,意想不到的结果完全有道理。进行了更改,代码就可以工作了!
      【解决方案3】:

      当我看到所需的输出时,我不禁想到使用 numpy 网格数据生成函数之一。

      import numpy
      first_column, second_column, third_column = numpy.mgrid[1:4,1:4,1:4]
      numpy.dstack((first_column.flatten(),second_column.flatten(),third_column.flatten()))
      Out[23]: 
      array([[[1, 1, 1],
          [1, 1, 2],
          [1, 1, 3],
          [1, 2, 1],
          [1, 2, 2],
          [1, 2, 3],
          [1, 3, 1],
          [1, 3, 2],
          [1, 3, 3],
          [2, 1, 1],
          [2, 1, 2],
          [2, 1, 3],
          [2, 2, 1],
          [2, 2, 2],
          [2, 2, 3],
          [2, 3, 1],
          [2, 3, 2],
          [2, 3, 3],
          [3, 1, 1],
          [3, 1, 2],
          [3, 1, 3],
          [3, 2, 1],
          [3, 2, 2],
          [3, 2, 3],
          [3, 3, 1],
          [3, 3, 2],
          [3, 3, 3]]])
      

      当然,这种特殊方法的实用性可能取决于您需要处理的输入的种类,但我怀疑这可能是一种有趣的构建数据的方法,而 numpy 对于这种事情来说非常快。据推测,如果您的输入列表有更多元素,您可以将更多的 min:max 参数输入 mgrid[],然后以类似的方式解包/堆栈。

      【讨论】:

        【解决方案4】:

        这是您的程序的简化版本,它可以工作。评论将随之而来。

        number_list = []
        
        def _adjust_counter_value(counter, n, max_count):
            """
            We want the counter to go from 1 to max_count, then start over at 1.
            This function adds n to the counter and then returns a tuple:
            (new_counter_value, carry_to_next_counter)
            """
            assert max_count >= 1
            assert 1 <= counter <= max_count
        
            # Counter is in closed range: [1, max_count]
            # Subtract 1 so expected value is in closed range [0, max_count - 1]
            x = counter - 1 + n
            carry, x = divmod(x, max_count)
        
            # Add 1 so expected value is in closed range [1, max_count]
            counter = x + 1
            return (counter, carry)
        
        def increment_counter(start, i, max_count):
            last = len(start) - 1 - i
            copy = start[:]  # make a copy of the start
        
            add = 1  # start by adding 1 to index
            for i_cur in range(last, -1, -1):
                copy[i_cur], add = _adjust_counter_value(copy[i_cur], add, max_count)
                if 0 == add:
                    return (copy, i, max_count)
            else:
                # if we have a carry out of the 0th position, we are done with the sequence
                return None
        
        def all_values(fresh_start, i, max_count):
            number_list.append(fresh_start)
            new_values = increment_counter(fresh_start,i,max_count)
            if new_values != None:
                all_values(*new_values)
        
        all_values([1,1,1],0,3)
        
        import itertools as it
        correct = [list(tup) for tup in it.product(range(1,4), range(1,4), range(1,4))]
        assert number_list == correct
        

        由于您希望计数器从 1 到 max_count(含),因此更新每个计数器有点棘手。您最初的解决方案是使用多个 if 语句,但在这里我创建了一个辅助函数,它使用 divmod() 来计算每个新数字。这让我们可以将任何增量添加到任何数字,并会找到该数字的正确进位。

        您的原始程序从未更改过i 的值,因此我修改后的程序也没有更改。您可以通过去掉i 并让increment_counter() 始终转到最后一个位置来进一步简化程序。

        如果您在没有调用breakreturn 的情况下将for 循环运行到最后,则else: 案例将在存在时运行。在这里,我添加了一个 else: 案例来处理列表中第 0 位的进位。如果第 0 位有进位,则表示我们已到达计数器序列的末尾。在这种情况下,我们返回None

        您的原始程序有点棘手。它在counter() 中有两个显式的return 语句,并在序列末尾有一个隐式返回。它确实返回None 表示递归可以停止,但它的执行方式对我来说太棘手了。我建议使用明确的return None,就像我展示的那样。

        请注意,Python 有一个模块itertools,其中包含一种生成此类计数器系列的方法。我用它来检查结果是否正确。

        我确定您写这篇文章是为了了解递归,但请注意,Python 并不是此类递归解决方案的最佳语言。 Python 的递归堆栈相对较浅,并且不会自动将尾递归转换为迭代循环,因此如果您的递归调用嵌套的次数足够多,这可能会导致 Python 内部的堆栈溢出。 Python 中的最佳解决方案是使用itertools.product(),就像我直接生成所需的计数器序列一样。

        由于您生成的序列是列表列表,而itertools.product() 生成元组,我使用列表推导将每个元组转换为列表,因此最终结果是列表列表,我们可以简单地使用 Python == 运算符来比较它们。

        【讨论】:

          猜你喜欢
          • 2020-07-30
          • 2013-10-18
          • 1970-01-01
          • 2010-09-28
          • 2012-09-22
          • 1970-01-01
          • 2020-05-05
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多