提问人:Ahmad Mudaafi' 提问时间:9/8/2022 最后编辑:Ahmad Mudaafi' 更新时间:9/8/2022 访问量:205
为什么 Python 中的临时变量会改变此 Pass-By-Sharing 变量的行为方式?
Why does a temporary variable in Python change how this Pass-By-Sharing variable behaves?
问:
第一次提问者在这里,所以一定要强调我的错误。
我正在磨练一些 Leetcode,并在 Python 中遇到了一种行为(与问题无关),我无法完全弄清楚,也无法用谷歌搜索。这尤其困难,因为我不确定我是否缺乏理解:
- 递归
- Python 中的运算符或一般的变量赋值
+=
- 或 Python 的通过共享行为
- 或者完全是别的东西
下面是简化的代码:
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
答:
我认为它来自你使用递归的方式。
当您调用您传递给更深潜方法的持有人时,是没有更新 val 的方法。
因此,您将在不修改持有者 val 的情况下调用 diveDeeper 函数 n 次,最终返回 1。holder.val += self.diveDeeper(holder, n-1)
您的示例将按如下方式执行:
- 调用 self.diveDeeper 且持有者未修改,n = 5;
- 调用 self.diveDeeper 且持有者未修改,n = 4;
- 调用 self.diveDeeper 且持有者未修改,n = 3;
- 调用 self.diveDeeper 且未修改持有者,n = 2;
- 调用 self.diveDeeper 且持有者未修改,n = 1;
- 调用 self.diveDeeper 且未修改 holder, n = 0;
- 返回 1
- 打印 holder.val 5 次,因为没有修改,因为上次递归返回 1
在(3)中,递归完成后,当你解开你的调用时,你会从最后一个diveDeeper调用中得到1,然后将其添加到holder中。在下一次展开时,您将执行相同的操作,从而在每次递归时将 val 从 1 递增。
你让我有点挣扎,但答案很简单。让我重新表述一下为什么会这样。
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.val
holder.val = 0 + 1
对于更改的顺序,我们首先进行突变,然后使用它来计算新的顺序。传递引用按预期工作。holder.val
评论
holder.val
holder.val
使用以下命令查看字节码: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.b
a.b
a.b
评论
对于案例 1。
holder.val += self.diveDeeper(holder, n-1)
此处的初始值为 0。所以实际上应该等于holder.val
holder.val
self.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 += returnVal
holder.val
holder.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)
在 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
这就是为什么你必须小心可变状态,你必须完美地理解操作的顺序。
评论