为什么 Python 中的临时变量会改变此 Pass-By-Sharing 变量的行为方式?

Why does a temporary variable in Python change how this Pass-By-Sharing variable behaves?

提问人:Ahmad Mudaafi' 提问时间:9/8/2022 最后编辑:Ahmad Mudaafi' 更新时间:9/8/2022 访问量:205

问:

第一次提问者在这里,所以一定要强调我的错误。

我正在磨练一些 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)开始,它似乎与运算符无关,为了简洁起见,我没有包括我尝试过的数十种变体,但到目前为止,它们都没有给我任何线索。非常感谢任何正确方向的指示(特别是如果我在求职面试中措手不及lmao)+=

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

Python 递归 传递引用

评论

3赞 kosciej16 9/8/2022
对于对此投反对票的人,您能详细说明一下吗?对我来说,这个问题是完全清楚的、有趣的,并且显示了一个完全不熟悉堆栈溢出的人的努力。

答:

0赞 Takezoshi 9/8/2022 #1

我认为它来自你使用递归的方式。

当您调用您传递给更深潜方法的持有人时,是没有更新 val 的方法。 因此,您将在不修改持有者 val 的情况下调用 diveDeeper 函数 n 次,最终返回 1。holder.val += self.diveDeeper(holder, n-1)

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

  1. 调用 self.diveDeeper 且持有者未修改,n = 5;
  2. 调用 self.diveDeeper 且持有者未修改,n = 4;
  3. 调用 self.diveDeeper 且持有者未修改,n = 3;
  4. 调用 self.diveDeeper 且未修改持有者,n = 2;
  5. 调用 self.diveDeeper 且持有者未修改,n = 1;
  6. 调用 self.diveDeeper 且未修改 holder, n = 0;
  7. 返回 1
  8. 打印 holder.val 5 次,因为没有修改,因为上次递归返回 1

在(3)中,递归完成后,当你解开你的调用时,你会从最后一个diveDeeper调用中得到1,然后将其添加到holder中。在下一次展开时,您将执行相同的操作,从而在每次递归时将 val 从 1 递增。

6赞 kosciej16 9/8/2022 #2

你让我有点挣扎,但答案很简单。让我重新表述一下为什么会这样。

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

我希望你现在看到发生了什么 - 以防万一它被评估为第一个变体。在每个递归步骤中,执行该行时将为 0。这就是为什么我们将分配 5 次 .+=holder.valholder.val = 0 + 1

对于更改的顺序,我们首先进行突变,然后使用它来计算新的顺序。传递引用按预期工作。holder.val

评论

0赞 Ahmad Mudaafi' 9/9/2022
啊,我现在明白了。 由于评估顺序,在突变之前读取。我喜欢你回答的简洁性,但我花了一段时间来处理,当读取/计算时,表达式会变成一个静态值。我希望你不介意,但我将@Jasmijn的答案标记为公认的答案,因为它们提供了额外的清晰度。不过,非常感谢答案。holder.valholder.val
2赞 Mechanic Pig 9/8/2022 #3

使用以下命令查看字节码: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

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

评论

0赞 Ahmad Mudaafi' 9/9/2022
我和下一个人一样喜欢汇编,但如果解释链接到 python 代码会更容易理解:') 不过,这是一个绝妙的答案,我的大脑并不是那么聪明。谢谢。
1赞 sahasrara62 9/8/2022 #4

对于案例 1。

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

此处的初始值为 0。所以实际上应该等于holder.valholder.valself.diveDeeper(holder, n-1)

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

当 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


注意:在每个函数调用中,值不存储在任何地方

而在案例 3 中

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

在这里,每个调用的值为 1。 即returnVal

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

所以我们得到了.不是'holder.val部分returnVal = 1

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

因此,当 n=0 时,对于最后一次调用,make = 1。 现在,当它返回 1 时,当 n=1 时进入递归链时,更新为 1 并且不再是 0 所以holder.val += returnValholder.valholder.val

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

即所有对最新调用的引用都已更新,不再是 0holder.val

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)

3赞 Jasmijn 9/8/2022 #5

在 Python 中,如果有 ,则首先计算。expression1() + expression2()expression1()

所以 1 和 2 实际上等价于:

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

现在,仅在递归调用之后进行修改,但您使用递归调用之前的值,这意味着无论迭代如何,.holder.valleft == 0

解决方案 3 等同于:

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

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

这就是为什么你必须小心可变状态,你必须完美地理解操作的顺序。