为啥函数可以修改调用者感知的某些参数,而不能修改其他参数?
Posted
技术标签:
【中文标题】为啥函数可以修改调用者感知的某些参数,而不能修改其他参数?【英文标题】:Why can a function modify some arguments as perceived by the caller, but not others?为什么函数可以修改调用者感知的某些参数,而不能修改其他参数? 【发布时间】:2010-10-09 04:18:27 【问题描述】:我试图了解 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]
【问题讨论】:
这里解释得很好nedbatchelder.com/text/names.html 【参考方案1】:当您在函数内部传递命令 n = 2 时,它会找到一个内存空间并将其标记为 2。但是如果您调用方法 append,您基本上是在引用位置 x(无论值是什么)并执行对此进行一些操作。
【讨论】:
【参考方案2】:正如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],因为在第 3 行中,您没有更改对象 [1,2,3] ,而是告诉 m 指向一个新的、不同的对象(使用 '='),而 n 仍然指向原始对象。
在第二个代码中,它将打印 n = [1, 2, 3, 4] m = [1, 2, 3, 4]。这是因为这里 m 和 n 在整个代码中仍然指向同一个对象,但是您使用 .append 方法修改了对象本身(m 指向)...请注意,第二个代码的结果将是无论你在第 3 行写 m.append(4) 还是 n.append(4) 都一样。
一旦你理解了这一点,剩下的唯一困惑就是真正理解,正如我所说,你的 f() 函数中的 n 和 x 和你的 main() 中的那些是不一样的,它们只是最初指向调用 f() 时指向同一个对象。
【讨论】:
【参考方案3】:请允许我再次编辑。这些概念是我通过尝试错误和互联网学习python的经验,主要是***。有错误,有帮助。
Python 变量使用引用,我认为引用是来自名称、内存地址和值的关系链接。
当我们做B = A
时,我们实际上创建了一个昵称A,现在A有2个名字,A和B。当我们调用B时,我们实际上是在调用A。我们创建一个值为的墨水其他变量,而不是创建一个新的相同值,这就是我们所说的引用。这种想法会导致两个问题。
当我们这样做时
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的值的相同地址。
当涉及到字符串、数字和元组时。价值和地址这对永远不会改变。当我们将str放到某个地址时,它立即被锁定,所有修改的结果都会放到其他地址中。
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,它是语法,等号。
【讨论】:
也许你只看代码就能理解发生了什么,但不知道为什么,@FMc 想知道为什么,而不是什么。【参考方案4】:我的一般理解是,任何对象变量(例如列表或字典等)都可以通过其函数进行修改。我相信您无法做的是重新分配参数 - 即在可调用函数中通过引用分配它。
这与许多其他语言一致。
运行以下简短脚本以查看其工作原理:
def func1(x, l1):
x = 5
l1.append("nonsense")
y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
【讨论】:
没有“对象变量”这样的东西。 Everything 在 Python 中是一个对象。有些对象公开了 mutator 方法(即它们是可变的),有些则没有。 兄弟最后的输出丢失了。结果如何?【参考方案5】:有些答案在函数调用的上下文中包含“复制”一词。我觉得很混乱。
Python 不会复制您在函数调用期间传递的对象永远。
函数参数是名称。当您调用函数时,Python 会将这些参数绑定到您传递的任何对象(通过调用者范围内的名称)。
对象可以是可变的(如列表)或不可变的(如 Python 中的整数、字符串)。您可以更改的可变对象。您无法更改名称,只能将其绑定到另一个对象。
您的示例不是关于scopes or namespaces,而是关于Python 中的naming and binding 和mutability of an object。
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
这是the difference between variables in other languages and names in Python上的精美图片。
【讨论】:
这篇文章帮助我更好地理解了这个问题,并提出了一种解决方法和一些高级用法:Default Parameter Values in Python @Gfy,我以前见过类似的例子,但对我来说,它并没有描述真实世界的情况。如果您正在修改传入的内容,则给它一个默认值是没有意义的。 @MarkRansom,我认为如果您想提供可选的输出目标,如:def foo(x, l=None): l=l or []; l.append(x**2); return l[-1]
。
Sebastian 代码的最后一行,上面写着“# 上面对原列表没有影响”。但在我看来,它只是对“n”没有影响,而是改变了 main() 函数中的“x”。我说的对吗?
@user17670: x = []
in f()
对主函数中的列表x
没有影响。我更新了评论,使其更加具体。【参考方案6】:
f
实际上并没有改变x
的值(它始终是对列表实例的相同引用)。相反,它会更改此列表的内容。
在这两种情况下,引用的副本都会传递给函数。在函数内部,
n
被分配了一个新值。只修改函数内部的引用,而不修改函数外部的引用。
x
没有被分配新值:函数内部和外部的引用都没有被修改。而是修改了x
的值。
由于函数内部和外部的x
都引用了相同的值,因此都可以看到修改。相比之下,函数内部和外部的n
在函数内部重新分配n
后引用了不同 值。
【讨论】:
“复制”具有误导性。 Python 没有像 C 这样的变量。Python 中的所有名称都是引用。您不能修改名称,您只能将其绑定到另一个对象,仅此而已。在 Python 中谈论可变和不可变 object 才有意义,而不是名称。 @J.F.塞巴斯蒂安:你的陈述充其量是误导性的。将数字视为参考是没有用的。 @dysfunctor:数字是对不可变对象的引用。如果您宁愿以其他方式考虑它们,则需要解释一堆奇怪的特殊情况。如果您认为它们是不可变的,则没有特殊情况。 @S.Lott:不管幕后发生了什么,Guido van Rossum 都在设计 Python 上付出了很多努力,以便程序员可以将数字视为......数字。 @J.F.,引用已复制。【参考方案7】:如果用完全不同的变量重新编写函数并且我们在它们上调用id,那么它很好地说明了这一点。起初我没有明白这一点,并用great explanation阅读了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。只是文章所说的相同底层结构的不同标签。
【讨论】:
【参考方案8】:Python 是按引用值复制的。一个对象占用内存中的一个字段,并且一个引用与该对象相关联,但它本身占用了内存中的一个字段。并且名称/值与引用相关联。在 python 函数中,它总是复制引用的值,因此在您的代码中,n 被复制为一个新名称,当您分配它时,它在调用者堆栈中有一个新空间。但是对于列表,名称也被复制了,但它引用了相同的内存(因为您从未为列表分配新值)。这就是python的魔法!
【讨论】:
【参考方案9】:你已经得到了很多答案,我大致同意 J.F. Sebastian 的观点,但你可能会发现这作为捷径很有用:
每当您看到 varname =
时,您就在函数范围内创建了一个新名称绑定。 varname
之前绑定的任何值都会丢失在此范围内。
每当您看到 varname.foo()
时,您就是在调用 varname
上的方法。该方法可能会更改 varname(例如 list.append
)。 varname
(或者,更确切地说,varname
命名的对象)可能存在于多个范围内,并且由于它是同一个对象,因此任何更改都将在所有范围内可见。
[注意global
关键字会为第一种情况创建一个例外]
【讨论】:
这些信息正是我所需要的,谢谢。 简洁、中肯、明确的答案。【参考方案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 按值传递地址变量存储。
【讨论】:
如果你以正确的方式考虑它,纯粹的指针传递值与引用传递没有太大区别...... 看看咕。如果你纯粹是通过引用传递,它会改变它的论点。不,Python 不是纯粹的引用传递语言。它按值传递引用。【参考方案11】:我将重命名变量以减少混淆。 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,并且类似地分配一个副本nmain 到 nf.
对于n,复制的值为1。
在 x 的情况下,复制的值不是文字列表 [0, 1, 2, 3]。它是对该列表的引用。 xf 和 xmain 指向同一个列表,所以当你修改 xf 时,你也在修改 xmain。
但是,如果您要编写如下内容:
xf = ["foo", "bar"]
xf.append(4)
你会发现 xmain 并没有改变。这是因为,在 xf = ["foo", "bar"] 行中,您已将 xf 更改为指向 new 列表。您对这个新列表所做的任何更改都不会影响 xmain 仍然指向的列表。
希望对您有所帮助。 :-)
【讨论】:
"在 n 的情况下,复制的值..." -- 这是错误的,这里没有进行复制(除非您计算引用)。相反,python 使用指向实际对象的“名称”。 nf 和 xf 指向 nmain 和 xmain,直到nf = 2
,其中名称 nf
更改为指向 2
。数字是不可变的,列表是可变的。【参考方案12】:
n 是一个 int(不可变),并且一个副本被传递给函数,因此在函数中您正在更改副本。
X 是一个列表(可变),指针 的副本被传递给函数,因此 x.append(4) 会更改列表的内容。但是,您在函数中说 x = [0,1,2,3,4],您不会更改 main() 中 x 的内容。
【讨论】:
观看“指针副本”的措辞。这两个地方都获得了对对象的引用。 n 是对不可变对象的引用; x 是对可变对象的引用。【参考方案13】:这是因为列表是一个可变对象。您没有将 x 设置为 [0,1,2,3] 的值,而是为对象 [0,1,2,3] 定义了一个标签。
你应该像这样声明你的函数 f():
def f(n, x=None):
if x is None:
x = []
...
【讨论】:
它与可变性无关。如果您使用x = x + [4]
而不是x.append(4)
,那么尽管列表是可变的,您也不会看到调用者发生任何变化。它与 if 确实发生了变异有关。
OTOH,如果你这样做了x += [4]
,那么x
会发生变异,就像x.append(4)
发生的情况一样,所以调用者会看到变化。以上是关于为啥函数可以修改调用者感知的某些参数,而不能修改其他参数?的主要内容,如果未能解决你的问题,请参考以下文章
在java中,引用数据不就是一种对象么?为啥在调用函数中不能进行修改数值??