为什么函数可以修改调用者所感知的一些参数,但不能修改其他参数?
- 2024-11-22 08:48:00
- admin 原创
- 161
问题描述:
我正在尝试理解 Python 对变量范围的处理方法。在此示例中,为什么 能够f()
改变 的值x
(如 内所见main()
),但不能改变 的值n
?
def f(n, x):
n = 2
x.append(4)
print('In f():', n, x)
def main():
n = 1
x = [0,1,2,3]
print('Before:', n, x)
f(n, x)
print('After: ', n, x)
main()
输出:
Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After: 1 [0, 1, 2, 3, 4]
参见:
如何通过引用传递变量?
Python 变量是指针吗?或者,它们是什么?
解决方案 1:
有些答案在函数调用的上下文中包含“复制”一词。我觉得这很令人困惑。
Python永远不会复制您在函数调用期间传递的对象。
函数参数是名称。当你调用一个函数时,Python 会将这些参数绑定到你传递的任何对象(通过调用者范围内的名称)。
对象可以是可变的(如列表)或不可变的(如 Python 中的整数和字符串)。可变对象可以更改。您不能更改名称,只能将其绑定到另一个对象。
您的示例与范围或命名空间无关,而是与 Python 中对象的命名、绑定和可变性有关。
def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
n = 2 # put `n` label on `2` balloon
x.append(4) # call `append` method of whatever object `x` is referring to.
print('In f():', n, x)
x = [] # put `x` label on `[]` ballon
# x = [] has no effect on the original list that is passed into the function
这里有一些关于其他语言中的变量和 Python 中的名称之间的区别的精美图片。
解决方案 2:
您已经得到了许多答案,并且我大体上同意 JF Sebastian 的观点,但您可能会发现这是一个有用的捷径:
只要您看到,您就会在函数范围内varname =
创建一个新的varname
名称绑定。之前绑定的任何值都会在此范围内丢失。
每当您看到varname.foo()
您正在调用一个方法时varname
。该方法可能会改变varname(例如list.append
)。 varname
(或者更确切地说,命名的对象varname
)可能存在于多个作用域中,并且由于它是同一个对象,因此任何更改都将在所有作用域中可见。
[请注意,global
关键字对第一种情况产生了例外]
解决方案 3:
f
实际上并没有改变 的值x
(它始终是对列表实例的相同引用)。相反,它改变了此列表的内容。
在这两种情况下,都会将引用的副本传递给函数。在函数内部,
n
被赋予一个新值。只有函数内部的引用被修改,而函数外部的引用不会被修改。x
未分配新值:函数内部和外部的引用均未修改。相反,x
的值被修改。
由于函数内部和外部都x
引用同一个值,因此两者都看到了修改。相反,在函数内部重新赋值后,函数n
内部和外部引用的值不同。n
解决方案 4:
我将重命名变量以减少混淆。n - > nf或nmain。x - > xf或xmain :
def f(nf, xf):
nf = 2
xf.append(4)
print 'In f():', nf, xf
def main():
nmain = 1
xmain = [0,1,2,3]
print 'Before:', nmain, xmain
f(nmain, xmain)
print 'After: ', nmain, xmain
main()
当调用函数f时,Python 运行时会复制xmain并将其分配给xf ,同样地,还会将nmai的副本分配 给nf。
对于n来说,复制的值是 1。
对于x来说,复制的值不是文字列表[0, 1, 2, 3]。它是对该列表的 引用。 xf和xmain指向同一个列表,因此当您修改xf时,您也会修改xmain。
但是,如果你要写类似这样的内容:
xf = ["foo", "bar"]
xf.append(4)
您会发现xmain并未发生变化。这是因为,在xf = ["foo", "bar"]行中,您已将xf更改为指向新列表。您对这个新列表所做的任何更改都不会对*xmain*仍指向的列表产生影响。
希望有所帮助。:-)
解决方案 5:
如果用完全不同的变量重写函数,然后我们对其调用id,那么它就很好地说明了这一点。我一开始没明白这一点,读了 jfs 的帖子,里面有很好的解释,所以我试着去理解/说服自己:
def f(y, z):
y = 2
z.append(4)
print ('In f(): ', id(y), id(z))
def main():
n = 1
x = [0,1,2,3]
print ('Before in main:', n, x,id(n),id(x))
f(n, x)
print ('After in main:', n, x,id(n),id(x))
main()
Before in main: 1 [0, 1, 2, 3] 94635800628352 139808499830024
In f(): 94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024
z 和 x 具有相同的 ID。正如文章所说,只是针对相同底层结构的不同标签。
解决方案 6:
这是因为列表是可变对象。您没有将 x 设置为 [0,1,2,3] 的值,而是为对象 [0,1,2,3] 定义了一个标签。
您应该像这样声明函数 f():
def f(n, x=None):
if x is None:
x = []
...
解决方案 7:
n 是一个 int(不可变),并且将副本传递给函数,因此在函数中您正在更改该副本。
X 是一个列表(可变),并且指针的副本被传递给函数,因此 x.append(4) 会更改列表的内容。但是,如果你在函数中说 x = [0,1,2,3,4],则不会在 main() 中更改 x 的内容。
解决方案 8:
我的一般理解是,任何对象变量(例如列表或字典等)都可以通过其函数进行修改。我认为您无法做的是重新分配参数 - 即在可调用函数中通过引用分配它。
这与许多其他语言一致。
运行以下简短的脚本来查看其工作原理:
def func1(x, l1):
x = 5
l1.append("nonsense")
y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
解决方案 9:
Python 是通过引用值进行复制的。对象占用内存中的一个字段,引用与该对象相关联,但它本身也占用内存中的一个字段。名称/值与引用相关联。在 Python 函数中,它总是复制引用的值,因此在您的代码中,n 被复制为一个新名称,当您分配它时,它在调用者堆栈中有一个新空间。但对于列表,名称也被复制了,但它引用的是相同的内存(因为您从未为列表分配新值)。这是 Python 中的魔法!
解决方案 10:
如果你以正确的方式思考,Python 是一种纯值传递语言。Python 变量存储对象在内存中的位置。Python 变量不存储对象本身。当你将变量传递给函数时,你传递的是变量指向的对象的地址的副本。
对比这两个函数
def foo(x):
x[0] = 5
def goo(x):
x = []
现在,当你在 shell 中输入
>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]
将其与 goo 进行比较。
>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]
在第一种情况下,我们将 cow 的地址副本传递给 foo,然后 foo 修改了驻留在那里的对象的状态。对象被修改了。
在第二种情况下,你将 cow 地址的副本传递给 goo。然后 goo 继续更改该副本。效果:无。
我把这称为粉红屋原则。如果你复制一份地址,然后告诉油漆工把该地址的房子漆成粉红色,你最终会得到一栋粉红色的房子。如果你把地址的副本交给油漆工,并告诉他将其更改为新地址,你房子的地址不会改变。
这个解释消除了很多困惑。Python 通过值传递变量存储的地址。
解决方案 11:
当您在函数内部传递命令 n = 2 时,它会找到一个内存空间并将其标记为 2。但是如果您调用方法 append,您基本上是引用位置 x(无论该值是什么)并对其执行一些操作。
解决方案 12:
正如 jouell 所说。这是一个指向什么的问题,我想补充的是,这也是 = 和 .append 方法所做之事之间的区别的问题。
当你在 main 中定义 n 和 x 时,你告诉它们指向 2 个对象,即 1 和 [1,2,3]。这就是 = 的作用:它告诉你的变量应该指向什么。
当调用函数 f(n,x) 时,您告诉两个新的局部变量 nf 和 xf 指向与 n 和 x 相同的两个对象。
当您使用“something”=“anything_new”时,您会更改“something”指向的内容。当您使用 .append 时,您会更改对象本身。
不知何故,即使您赋予它们相同的名称,main() 中的 n 和 f() 中的 n 也不是同一个实体,它们最初只是指向同一个对象(实际上 x 也是如此)。更改其中一个指向的内容不会影响另一个。但是,如果您更改对象本身,这将影响两个变量,因为它们都指向同一个(现已修改)对象。
让我们说明一下方法 .append 和 = 之间的区别,无需定义新函数:
比较
m = [1,2,3]
n = m # this tells n to point at the same object as m does at the moment
m = [1,2,3,4] # writing m = m + [4] would also do the same
print('n = ', n,'m = ',m)
到
m = [1,2,3]
n = m
m.append(4)
print('n = ', n,'m = ',m)
在第一个代码中,它将打印 n = [1, 2, 3] m = [1, 2, 3, 4],因为在第三行中,您没有更改对象 [1,2,3],而是告诉 m 指向一个新的、不同的对象(使用 '='),而 n 仍然指向原始对象。
在第二段代码中,它会打印出 n = [1, 2, 3, 4] m = [1, 2, 3, 4]。这是因为在这里,m 和 n 在整个代码中仍然指向同一个对象,但是你使用 .append 方法修改了对象本身(m 指向的那个对象)...请注意,无论你在第三行写 m.append(4) 还是 n.append(4),第二段代码的结果都是相同的。
一旦你理解了这一点,剩下的唯一困惑就是真正理解,正如我所说的,f() 函数中的 n 和 x 与 main() 中的 n 和 x 并不相同,它们只是在你调用 f() 时最初指向同一个对象。
解决方案 13:
请允许我再次编辑。这些概念是我通过 try error 和互联网(主要是 stackoverflow)学习 Python 的经验。其中有错误,也有帮助。
Python 变量使用引用,我认为引用是从名称、内存地址和值的关系链接。
当我们这样做时B = A
,我们实际上创建了 A 的一个昵称,现在 A 有 2 个名字,A 和 B。当我们调用 B 时,我们实际上是在调用 A。我们创建了一个指向其他变量值的引用,而不是创建一个新的相同值,这就是我们所说的引用。这种想法会导致 2 个问题。
当我们这样做的时候
A = [1]
B = A # Now B is an alias of A
A.append(2) # Now the value of A had been changes
print(B)
>>> [1, 2]
# B is still an alias of A
# Which means when we call B, the real name we are calling is A
# When we do something to B, the real name of our object is A
B.append(3)
print(A)
>>> [1, 2, 3]
当我们将参数传递给函数时会发生以下情况
def test(B):
print('My name is B')
print(f'My value is {B}')
print(' I am just a nickname, My real name is A')
B.append(2)
A = [1]
test(A)
print(A)
>>> [1, 2]
我们将 A 作为函数的参数传递,但该函数中该参数的名称是 B。名称不同,但相同。
因此,当我们这样做时B.append
,我们正在做A.append
当我们将参数传递给函数时,我们不是传递变量,而是传递别名。
那么,两个问题就来了。
等号总是创建一个新名称
A = [1]
B = A
B.append(2)
A = A[0] # Now the A is a brand new name, and has nothing todo with the old A from now on.
B.append(3)
print(A)
>>> 1
# the relation of A and B is removed when we assign the name A to something else
# Now B is a independent variable of hisown.
等号是明确的全新名称的陈述,
这是我的脑震荡部位
A = [1, 2, 3]
# No equal sign, we are working on the origial object,
A.append(4)
>>> [1, 2, 3, 4]
# This would create a new A
A = A + [4]
>>> [1, 2, 3, 4]
和函数
def test(B):
B = [1, 2, 3] # B is a new name now, not an alias of A anymore
B.append(4) # so this operation won't effect A
A = [1, 2, 3]
test(A)
print(A)
>>> [1, 2, 3]
# ---------------------------
def test(B):
B.append(4) # B is a nickname of A, we are doing A
A = [1, 2, 3]
test(A)
print(A)
>>> [1, 2, 3, 4]
第一个问题是
等式的左边总是一个全新的名称、新的变量,
除非右侧是名称,例如
B = A
,这只会创建别名
第二个问题,有些东西是永远无法改变的,我们无法修改原来的,只能创建一个新的。
这就是我们所说的不可变。
当我们这样做时A= 123
,我们创建一个包含名称、值和地址的字典。
当我们这样做时B = A
,我们将地址和值从 A 复制到 B,对 B 的所有操作都会影响 A 的值的相同地址。
字符串、数字、元组,值和地址的配对永远不能改变,当我们把一个字符串放到某个地址时,它马上就被锁定了,所有修改的结果都会放到其他地址中。
A = 'string'
将创建一个受保护的值和地址来存储字符串 'string' 。目前,没有内置函数或方法可以使用类似语法修改字符串list.append
,因为此代码修改了地址的原始值。
字符串、数字或元组的值和地址受到保护、锁定、不可变。
我们对字符串所做的所有操作都是通过语法A = B.method
,我们必须创建一个新名称来存储新的字符串值。
如果您仍然感到困惑,请扩展此讨论。这个讨论帮助我一劳永逸地弄清楚可变/不可变/引用/参数/变量/名称,希望这也可以对某些人有所帮助。
我已经多次修改了我的答案并且意识到我不需要说任何话,python 已经解释了自己。
a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'
a = a.replace('t', '_')
print(a)
>>> 's_ring'
b = 100
b + 1
print(b)
>>> 100
b = b + 1
print(b)
>>> 101
def test_id(arg):
c = id(arg)
arg = 123
d = id(arg)
return
a = 'test ids'
b = id(a)
test_id(a)
e = id(a)
# b = c = e != d
# this function do change original value
del change_like_mutable(arg):
arg.append(1)
arg.insert(0, 9)
arg.remove(2)
return
test_1 = [1, 2, 3]
change_like_mutable(test_1)
# this function doesn't
def wont_change_like_str(arg):
arg = [1, 2, 3]
return
test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)
这个魔鬼不是引用/值/可变或不可变/实例、名称空间或变量/列表或 str,而是语法、等号。