为什么函数可以修改调用者感知到的一些参数,而不能修改其他参数?

我试图理解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]
219714 次浏览

f实际上不会改变x的值(它始终是对列表实例的相同引用)。相反,它改变了这个列表的内容

在这两种情况下,参考资料副本都被传递给函数。在函数内部,

  • n被赋给一个新值。只修改函数内部的引用,而不修改函数外部的引用。
  • x不会被赋一个新值:函数内部和外部的引用都不会被修改。相反,x价值被修改。

由于函数内部和外部的x都指向相同的值,所以两者都看到了修改。相比之下,函数内部和外部的n指的是在函数内部重新分配n后的不同的值。

这是因为列表是一个可变对象。你不是将x设置为[0,1,2,3]的值,你是在为对象[0,1,2,3]定义一个标签。

你应该这样声明你的函数f():

def f(n, x=None):
if x is None:
x = []
...

N是一个int(不可变),并且一个副本被传递给函数,因此在函数中您正在更改副本。

X是一个列表(可变),的指针的副本被传递给函数,因此x.a pend(4)改变列表的内容。然而,你在你的函数中说x =[0,1,2,3,4],你不会在main()中改变x的内容。

我将重命名变量以减少混乱。n -> nfnmainx -> xfxmain:

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]。它是该列表的参考xfxmain指向同一个列表,所以当你修改xf时,你也在修改xmain

然而,如果你要写这样的东西:

    xf = ["foo", "bar"]
xf.append(4)

你会发现xmain没有改变。这是因为,在Xf = ["foo", "bar"]行中,您已经将xf更改为指向列表。对这个新列表所做的任何更改都不会对xmain仍然指向的列表产生影响。

希望这能有所帮助。: -)

有些答案包含“复制”一词;在函数调用的上下文中。我觉得很困惑。

Python不会复制函数调用期间传递的对象 never

函数参数为的名字。当你调用一个函数时,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中的名称之间的区别上的一些不错的图片。

你已经得到了一些答案,我基本上同意J.F.塞巴斯蒂安的观点,但你可能会发现这是一条有用的捷径:

任何时候看到varname =,都是在函数的作用域内创建名称绑定。varname之前绑定的值将丢失在此范围内

任何时候你看到varname.foo(),你都在调用varname上的一个方法。该方法可以改变varname(例如list.append)。varname(或者,更确切地说,varname命名的对象)可能存在于多个作用域中,并且由于它是同一个对象,任何更改都将在所有作用域中可见。

[注意global关键字为第一个情况创建了一个异常]

如果你正确地思考,Python是一种纯粹的值传递语言。python变量在内存中存储对象的位置。Python变量不存储对象本身。当你将一个变量传递给一个函数时,你是在传递变量所指向的对象地址的复制

契约这两种功能

def foo(x):
x[0] = 5


def goo(x):
x = []

现在,当你输入外壳的时候

>>> 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继续更改该副本。效果:没有。

我称之为粉红房子原则。如果你把你的地址复印一份,并告诉a 如果油漆工把那个地址的房子漆成粉红色,你就会得到一座粉红色的房子。 如果你给油漆工一份你的地址复印件,让他把它改成一个新地址,

.你的房子的地址不会改变

这种解释消除了许多困惑。Python将地址变量按值存储。

Python是根据引用值复制的。对象占用内存中的一个字段,引用与该对象相关联,但其本身占用内存中的一个字段。name/value与引用相关联。在python函数中,它总是复制引用的值,所以在你的代码中,n被复制为一个新的名称,当你赋值时,它在调用者堆栈中有一个新的空间。但对于列表,名称也被复制,但它指向相同的内存(因为您从未为列表分配新值)。这就是python的神奇之处!

如果函数是用完全不同的变量重写的,并且我们对它们调用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。就像文章所说的那样,不同的标签对应相同的底层结构。

我的一般理解是,任何对象变量(如列表或字典等)都可以通过其函数进行修改。我相信你不能做的是重新分配形参-即,在可调用函数中通过引用分配它。

这与许多其他语言是一致的。

运行下面的简短脚本,看看它是如何工作的:

def func1(x, l1):
x = 5
l1.append("nonsense")


y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)

请允许我重新编辑。这些概念是我通过尝试错误和internet学习python的经验,主要是stackoverflow。有错误也有帮助。

Python变量使用引用,我认为引用是名称、内存地址和值的关系链接。

当我们执行B = A时,我们实际上创建了a的昵称,现在a有两个名字,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 当我们向函数传递参数时,我们传递的不是一个变量,而是一个别名

这里有两个问题。

  1. 等号总是创建一个新名称
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]

第一个问题是

  1. 方程的左边总是一个全新的名字,新的变量,

  2. 除非右边是一个名字,比如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)


这个魔鬼不是引用/值/可变或not /实例,名称空间或变量/列表或str,它是语法,等号。

正如jouell所说。这是一个什么指向什么的问题,我想补充的是,这也是一个=做什么和.append方法做什么之间的区别的问题。

  1. 当你在main中定义n和x时,你告诉它们指向两个对象,即1和[1,2,3]。这就是=的作用:它告诉你的变量应该指向什么。

  2. 当你调用函数f(n,x)时,你告诉两个新的局部变量nf和xf指向与n和x相同的两个对象。

  3. 当你用“;something"=;anything_new";点。当你使用.append时,你改变了对象本身。

  4. 不管怎样,即使你给了它们相同的名字,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.p append(4)还是n.p append(4),第二段代码的结果都是一样的。

一旦你理解了这一点,剩下的唯一困惑就是真正理解,正如我所说,f()函数中的n和x与main()中的n和x并不相同,它们只是在调用f()时最初指向同一个对象。

当你在函数中传递命令n = 2时,它会找到一个内存空间,并将其标记为2。但是如果你调用append方法,你基本上是在引用位置x(不管它的值是什么)并对它做一些操作。