为什么Python中的临时变量会影响这个通过共享传递的变量的行为?

8

我是第一次提问者,如果我的问题有误请指出。

我正在刷Leetcode时遇到了一个问题(与问题无关),在Python中我无法理解或通过谷歌搜索找到答案。这特别困难,因为我不确定我的理解不足是否在于:

  1. 递归
  2. Python中的+=运算符或变量分配等一般操作
  3. 还是Python的按共享传递行为
  4. 或仅仅是其他某些原因

以下是简化的代码:

class Holder:
    def __init__(self, val=0):
         self.val = val

class Solution:
    def runThis(self):
        holder = Holder()
        self.diveDeeper(holder, 5)
        return 
        
    def diveDeeper(self, holder, n):
        if n==0:
            return 1

        # 1) Doesn't result in mutation
        holder.val += self.diveDeeper(holder, n-1)

        # 2) Also doesn't result in mutation
        # holder.val = holder.val + self.diveDeeper(holder, n-1)

        # 3) !! Results in mutations
        # returnVal = self.diveDeeper(holder, n-1)
        # holder.val += returnVal

        print(holder.val)
        return 1

a = Solution()
a.runThis()

所以,我的主要困惑在于(1)和(3)在语义上看起来完全相同,但结果却完全不同:

================ RESTART: Case 1 ===============
1
1
1
1
1
>>> 
================ RESTART: Case 3 ===============

1
2
3
4
5
>>> 

从(2)中看起来似乎与 += 操作符无关,为了简洁起见,我没有包含我尝试的数十种变体,但到目前为止没有给我任何线索。非常感谢任何指向正确方向的指针(特别是在面试时可能会被蒙蔽的情况下)

PS:如果相关,我正在使用Python 3.8.2


3
对于那些对此给出了负面投票的人,您能否详细解释一下?对我来说,这个问题非常清晰、有趣,并展示了一个完全是新手的人在stackoverflow上的努力。 - kosciej16
5个回答

6
你让我费了些劲,但答案其实很简单。让我重新表述一下为什么会发生这种情况。
holder.val = holder.val + self.diveDeeper(holder, n - 1) # prints 1 1 1 1 1
holder.val = self.diveDeeper(holder, n - 1) + holder.val # prints 1 2 3 4 5

我希望你现在明白了发生了什么 - 在+=的情况下,它将计算为第一种变体。在每个递归步骤中,执行该行时holder.val将为0。因此,我们会五次赋值holder.val = 0 + 1

改变顺序后,我们先改变holder.val然后再用它来计算新的值。按引用传递的方式正常工作。


啊,我现在明白了。由于评估顺序的原因,在变异之前读取了holder.val。我喜欢你回答的简洁性,但是我花了一段时间才能理解当读取/评估holder.val时,表达式变成了静态值。希望你不介意,但是我将@Jasmijn的答案标记为被接受的,因为他们提供了额外的清晰度。非常感谢你的回答。 - Ahmad Mudaafi'

3
在Python中,如果你有expression1() + expression2(),则会先计算expression1()
因此,1和2实际上是等价的:
left = holder.val
right = self.diveDeeper(holder, n - 1)
holder.val = left + right

现在,holder.val 只在递归调用之后修改,但是你使用了递归调用之前的值,这意味着无论迭代多少次,left == 0

你的解决方案3等同于:

right = self.diveDeeper(holder, n - 1)
left = holder.val
holder.val = left + right

因此,在评估left = holder.val之前进行了递归调用,这意味着left现在是上一次迭代结果的总和。

这就是为什么在处理可变状态时你必须小心,你必须完全理解操作顺序的原因。


2

使用dis查看字节码:

>>> from dis import dis
>>> dis('a.b += c()')
  1           0 LOAD_NAME                0 (a)
              2 DUP_TOP
              4 LOAD_ATTR                1 (b)
              6 LOAD_NAME                2 (c)
              8 CALL_FUNCTION            0
             10 INPLACE_ADD
             12 ROT_TWO
             14 STORE_ATTR               1 (b)
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

>>> dis('''r = c()
... a.b += r''')
  1           0 LOAD_NAME                0 (c)
              2 CALL_FUNCTION            0
              4 STORE_NAME               1 (r)

  2           6 LOAD_NAME                2 (a)
              8 DUP_TOP
             10 LOAD_ATTR                3 (b)
             12 LOAD_NAME                1 (r)
             14 INPLACE_ADD
             16 ROT_TWO
             18 STORE_ATTR               3 (b)
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE


一个明显的区别是,前者在调用函数之前已经加载了a.b的值,在每次递归中都为0。因为数字是不可变对象,预先加载的数字在每次加法和存储后不会改变。后者是在调用函数后加载a.b的值,这导致a.b的值在每次递归后更新。

我和其他人一样喜欢汇编语言,但如果解释能够与Python代码相互关联的话,那就更容易理解了 :') 这是一个很棒的答案,只是我的大脑不够聪明。谢谢。 - Ahmad Mudaafi'

1

对于案例1。

        holder.val += self.diveDeeper(holder, n-1)

这里holder.val的初始值为0。因此holder.val应该等于self.diveDeeper(holder, n-1)的值。

由于此函数仅返回1且holder.val仍然是局部变量,因此在所有函数调用中它将保持为0(最初)

当N=5时

holder.vaL = 0 + self.diveDeeper(holder, 5)
           = 0 + (0 + self.diveDeeper(holder, 4))
           = 0 + (0 + (0 + self.diveDeeper(holder, 3)))
           = 0 + (0 + (0 + (0 + self.diveDeeper(holder, 2))))
           = 0 + (0 + (0 + (0 + (0 +self.diveDeeper(holder, 1)))))
           = 0 + (0 + (0 + (0 + (0 +1))))
           = 1


请注意,在每个函数调用中,值都没有存储在任何地方。

然而,在第三种情况下

returnVal = self.diveDeeper(holder, n-1)
holder.val += returnVal

在这里,每次调用returnVal的返回值都将为1。

n = 5, returnVal = self.diveDeeper(holder, 4) 
                 = self.diveDeeper(holder, 3) 
                 = self.diveDeeper(holder, 2)
                 = self.diveDeeper(holder, 1)
                 = self.diveDeeper(holder, 0)
                 = 1

所以我们得到了returnVal = 1。现在是`holder.val`部分。

由于holder是一个对象,在函数调用期间它会不断地传递,因此它将始终保持不变。

因此,在最后一次调用时,当n=0时,holder.val += returnVal使holder.val=1。 现在当它返回1并进入递归链时,当n=1时,holder.val被更新为1,不再是0了,因此

holder.val = 1 + self.diveDeeper(holder, 1)

即所有关于holder.val的引用都已更新为最新调用,并且不再是0。

ie when n = 0, holder.val = 0
        n =1, holder.val = holder.val ( value of holder.val when n= 0, ie 0) + 1 (1 is value of self.diveDeeper(holder, 0)
        n= 2 holder.val = holder.val ( value of holder.val when n= 1, ie 1) + 1 (1 is value of self.diveDeeper(holder, 1)
        n= 3 holder.val = holder.val ( value of holder.val when n= 2, ie 2) + 1 (1 is value of self.diveDeeper(holder, 2)
         n= 4 holder.val = holder.val ( value of holder.val when n= 3, ie 3) + 1 (1 is value of self.diveDeeper(holder, 3)
         n= 5 holder.val = holder.val ( value of holder.val when n= 4, ie 4) + 1 (1 is value of self.diveDeeper(holder, 4)


0

我认为这是由于您使用递归的方式导致的。

当您调用 holder.val += self.diveDeeper(holder, n-1) 时,您传递给 diveDeeper 方法的 holder 是没有更新 val 的那个。 因此,您将调用 n 次 diveDeeper 函数而不修改 holder val,并在最后返回 1。

您的示例将按以下方式执行:

  1. 使用未修改的 holder 和 n = 5 调用 self.diveDeeper;
  2. 使用未修改的 holder 和 n = 4 调用 self.diveDeeper;
  3. 使用未修改的 holder 和 n = 3 调用 self.diveDeeper;
  4. 使用未修改的 holder 和 n = 2 调用 self.diveDeeper;
  5. 使用未修改的 holder 和 n = 1 调用 self.diveDeeper;
  6. 使用未修改的 holder 和 n = 0 调用 self.diveDeeper;
  7. 返回 1
  8. 打印 holder.val 5 次,它们没有被修改,因为您上一次递归返回了 1
在(3)中,递归完成后,当您取消包装调用时,将从最后一个diveDeeper调用中获得1,然后将其添加到holder中。在下一次取消包装时,您将执行相同的操作,导致在每个递归中将val增加1。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接