【问题标题】:recursive calls with list parameters (python)带有列表参数的递归调用(python)
【发布时间】:2021-12-20 05:52:04
【问题描述】:

所以 a 有两个函数进行递归调用,第一个调用修改列表 l1,然后将其传递给第二个调用。在 f1 第二次递归调用中收到修改后的列表 [1,1]:

b 2 []
b 1 []
a 1 [1, 1]
b 1 [1, 1]
a 1 [1, 1, 1, 1]
a 2 [1, 1, 1, 1, 1, 1]

但在 f2 第二次调用中收到未修改的空列表:

b 2 []
b 1 []
a 1 [1, 1]
b 1 []
a 1 [1, 1]
a 2 [1, 1, 1, 1]

为什么? f2(f1) 不是在这两种情况下都被连续调用了吗?

def f1(l1=[],num = 2):
    if num == 0: return[1]
    print('b',num,l1)
    l1 = [*l1, *f1(l1, num-1)]
    l1 = [*l1, *f1(l1, num -1)]
    print('a',num,l1)
    return l1

def f2(l1=[],num = 2):
    if num == 0: return[1]
    print('b',num,l1)
    l1 = [*l1, *f2(l1, num-1),*f2(l1, num -1)]
    print('a',num,l1)
    return l1


       

如果列表理解是一个问题(你不能在其中修改列表),为什么在这里修改它:

def f3(l1):
    print(l1)
    l1 +=[1]
    return l1
l1 = []
l1 = [*l1, *f3(l1), *f3(l1)]

print(l1)

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

【问题讨论】:

  • 不确定您要达到的目标。函数 f1 和 f2 在功能上是不同的。因此,使用相同的输入值连续调用 f1 和 f2 可能会产生不同的结果
  • 我只是想了解它为什么会这样工作。在 f3 示例中,一个修改后的列表在列表理解中是过去的,尽管它看起来很像 f2。
  • 这里不相关(您似乎不会在没有第一个参数的情况下调用任何一个函数),但不要使用列表作为默认参数值。见stackoverflow.com/questions/1132941/…

标签: python list recursion


【解决方案1】:

函数f1 创建一个全新的列表,然后在第二次调用f1 时使用该新列表。函数f2 只会将原始列表传递给它对f2 的调用。

展开,f1,逻辑如下:

def f1(l1=[],num = 2):
    if num == 0: return[1]
    print('b',num,l1)
    
    # Here, *f1(l1, num-1) uses the original value of
    # l1, then expands it.
    # *l1 expands the original value of l1.
    # The [...] creates a completely new list,
    # and finally, l1 = ... assigns that completely new
    # list to the value of l1.
    l1 = [*l1, *f1(l1, num-1)]
    
    # Here, the l1 in *f1(l1, num -1) refers to the
    # result of the assignment above, l1 = ...
    #
    # *l1 also refers to the result of the assignment
    # above.
    #
    # Again, [...] creates a completely new list,
    # then l1 = ... assigns that new list to the name l1.
    l1 = [*l1, *f1(l1, num -1)]
    print('a',num,l1)
    return l1
    
# This function is identical
def f1(first_list = [], num = 2):
    if num == 0: return [1]
    print('b', num, first_list)
    
    second_list = [*first_list, *f1(first_list, num - 1)]
    
    third_list = [*second_list, *f1(second_list, num - 1)]
    print('a', num, third_list)
    return third_list

f2 中的逻辑如下:

def f2(l1=[],num = 2):
    if num == 0: return[1]
    print('b',num,l1)
    
    # Here, both *f2(l1, num-1) calls refer to the original l1
    # passed in the argument list.
    #
    # *l1 refers to the original l1 passed in the argument list,
    # too.
    #
    # The [...] creates a whole new list,
    # then l1 = ... assigns that new list to the name l1.
    l1 = [*l1, *f2(l1, num-1),*f2(l1, num -1)]
    print('a',num,l1)
    return l1

# This function is identical
def f2(first_list = [], num = 2):
    if num == 0: return [1]
    print('b', num, first_list)
    
    second_list = [*first_list, *f2(first_list, n - 1), *f2(first_list, n - 1)]
    print('a', num, second_list)
    return second_list

f2f3 之间的区别在于,在 f2 中,您没有调用任何修改列表的代码:您正在创建一个全新的列表,然后重新分配名称 l1 以引用到新列表。在f3 中,您并没有创建一个全新的列表,您只是在原地修改列表。

让我们举一个简化的例子来说明这一点:

>>> def set_value(first_list):
...     first_list = [ 2 ]
... 
>>> mylist = [3, 4]
>>> set_value(mylist)
>>> mylist
[3, 4]
>>> 
>>> def set_value(first_list):
...     first_list.clear()
...     first_list.append(2)
... 
>>> mylist
[3, 4]
>>> set_value(mylist)
>>> mylist
[2]
>>> 

+= 操作符修改左边的列表,而= 根本不修改列表。 l1 += ...... 添加到 l1 所指列表的末尾。 l1 = ... 只是更改了名称 l1 所指的内容:您可以使其引用整数、字典、对象或其他任何内容。

【讨论】:

  • 好吧,我想我现在明白了。重新绑定名称 l1 不会更改它所指的真实对象(与 += 运算符不同)。但是为什么 f2 在第二次调用 l1 = [*l1, *f2(l1, num-1),*f2(l1, num -1)] 时不检查 l1 指的是什么?就好像在重新绑定名称 l1 之前(在第一个 f2 调用中),未修改的列表已粘贴到第二个 f2 上。如果这有意义....
  • 确实如此:问题是,l1 所指的是最后的改变。首先,调用其中一个 f2 调用,而 l1 不变;然后,另一个 f2 被调用,而 l1 仍然不变;然后,*l1 解包(仍然不变)l1,解包两个 f2 调用的结果,并创建一个包含结果的新列表 [...]。只有这样,最后,l1 所引用的列表才会设置为这个全新的列表。
  • 为了将l1分配给全新的列表,首先必须创建列表;要创建列表,首先需要找到*l1*f2(...)*f2(...) 的结果。
  • 我的意思是在第一次f2 调用期间(当它遇到f2(l1,0) 情况时)在调用堆栈中重新分配l1,所以所有新的l1 引用调用堆栈不会影响原始的l1 = [] 引用,它们只是“堆栈调用的本地”的新列表?
  • 完全正确。如果您在这些子调用中就地修改了l1,使用+=.append 或类似名称,您会在顶部调用中看到它。但是,这些子调用中的每一个都只是将名称l1 在该调用中重新分配给在在该调用中创建的新列表。不过,我建议不要在递归调用中进行这样的修改,因为它使逻辑更难推理。做一些处理,然后返回一个新值,然后在顶部调用中使用这个新值更容易理解。
【解决方案2】:

f1 中,第二个调用接收l1 的修改内容,但在f2 它接收原始(未修改)内容,因为在执行推导时列表未修改,但仅在最后一次结果列表已完成构建。

【讨论】:

  • 我添加了另一个列表理解示例,它的工作方式不同。那么这有什么不同呢?
猜你喜欢
  • 2021-01-12
  • 1970-01-01
  • 2010-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-21
  • 2021-01-12
  • 2011-02-05
相关资源
最近更新 更多