关于python:为什么一个函数可以修改调用者所感知的一些参数,而不是其他参数?

Why can a function modify some arguments as perceived by the caller, but not others?

我试图理解Python对变量范围的方法。在这个例子中,为什么f()能够改变x的值,正如在main()中看到的那样,而不是n的值?

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

输出:

1
2
3
Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]


有些答案在函数调用的上下文中包含单词"copy"。我觉得很困惑。

python不会复制在函数调用期间传递的对象。

函数参数是名称。当调用函数时,python将这些参数绑定到您传递的任何对象(通过调用方作用域中的名称)。

对象可以是可变的(如列表)或不可变的(如整数、Python中的字符串)。可以更改的可变对象。不能更改名称,只需将其绑定到另一个对象即可。

您的示例不是关于范围或名称空间,而是关于Python中对象的命名、绑定和可变性。

1
2
3
4
5
6
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.Sebastian的观点,但你可能会发现这是一个很有用的捷径:

每当看到varname =时,都会在函数的作用域内创建一个新的名称绑定。在这一范围内,以前绑定到的varname的任何值都将丢失。

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

[请注意,global关键字创建了第一种情况的异常]


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

在这两种情况下,都会将引用的副本传递给函数。在函数内部,

  • n被分配一个新值。只修改函数内部的引用,而不修改函数外部的引用。
  • x没有得到一个新的值:函数内部和外部的引用都没有被修改。相反,修改了x的值。

因为函数内部和外部的x都引用相同的值,所以都可以看到修改。相比之下,函数内部和外部的n引用了在函数内部重新分配n之后的不同值。


我将重命名变量以减少混淆。n->nf或nmain。x->xf或xmain:

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

但是,如果你要写如下的东西:

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

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

希望有帮助。-)


n是一个int(不可变),一个副本传递给函数,所以在函数中,您要更改副本。

x是一个列表(可变),指针的一个副本被传递给函数,因此x.append(4)更改列表的内容。但是,您在函数中说x=[0,1,2,3,4]时,不会更改main()中x的内容。


这是因为列表是可变对象。您没有将x设置为[0,1,2,3]的值,而是定义了对象[0,1,2,3]的标签。

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

1
2
3
4
def f(n, x=None):
    if x is None:
        x = []
    ...


如果函数是用完全不同的变量重新编写的,并且我们对它们调用id,那么它就很好地说明了这一点。我一开始并没有收到这个消息,我读了JFS的帖子并给出了很好的解释,所以我试图理解/说服自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。正如本文所说,相同的底层结构具有不同的标记。


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

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

运行以下简短脚本以查看其工作原理:

1
2
3
4
5
6
7
8
9
10
11
    def func1(x, l1):
    x = 5
    l1.append("nonsense")
</p>

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

代码>

`


python是按引用值复制的。一个对象在内存中占有一个字段,并且一个引用与该对象相关联,但它本身在内存中占有一个字段。名称/值与引用关联。在python函数中,它总是复制引用的值,因此在代码中,n被复制为一个新名称,当您分配它时,它在调用方堆栈中有一个新的空间。但对于列表,名称也会被复制,但它指向相同的内存(因为您从未为列表分配新值)。这是Python的魔法!


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

禁止这两种功能

1
2
3
4
5
def foo(x):
    x[0] = 5

def goo(x):
    x = []

现在,当你在壳里打字的时候

1
2
3
4
>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

把这个和goo比较一下。

1
2
3
4
>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

在第一种情况下,我们将cow地址的副本传递给foo,foo修改了驻留在那里的对象的状态。对象被修改。

在第二种情况下,您将一份Cow地址的副本传递给Goo。然后顾继续修改那个副本。效果:无。

我称之为粉红屋原则。如果你把你的地址复印一份然后告诉画家把那个地址的房子漆成粉红色,你就会得到一个粉红色的房子。如果你给油漆工一份地址副本,告诉他改成新地址,你家的地址不变。

这个解释消除了很多混乱。python传递按值存储的地址变量。