为什么函数可以修改调用者认为的某些自变量,而不能修改其他自变量?


181

我正在尝试了解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]

Answers:


209

一些答案在函数调用的上下文中包含单词“ copy”。我感到困惑。

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中的名称之间的区别的好照片。


3
本文帮助我更好地理解了该问题,并提出了一种解决方法和一些高级用法:Python中的默认参数值
Gfy 2011年

@Gfy,我之前也看过类似的例子,但对我而言,这并没有描述现实情况。如果您要修改传入的内容,则默认为默认值是没有意义的。
马克·兰瑟姆

@MarkRansom,如果您要提供可选的输出目标(如),我认为这确实有意义def foo(x, l=None): l=l or []; l.append(x**2); return l[-1]
Janusz Lenar '08年

对于塞巴斯蒂安的代码的最后一行,它说“#以上对原始列表没有影响”。但是在我看来,它仅对“ n”没有影响,但是改变了main()函数中的“ x”。我对么?
user17670

1
@ user17670:x = []in 对主功能中f()的列表没有影响x。我已经更新了评论,以使其更加具体。
jfs

15

您已经有了许多答案,并且我在很大程度上同意JF Sebastian的观点,但是您可能会觉得这很实用:

每次看到时varname =,您都在函数范围内创建新的名称绑定。在此范围内,varname丢失的绑定值将丢失。

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

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


13

f实际上并不会更改x(始终与列表实例的引用相同)值。而是,它更改了此列表的内容

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

  • n被分配一个新值。仅修改函数内部的引用,而不修改函数外部的引用。
  • x不会被分配新值:函数内部和外部的引用均未修改。而是会修改x

由于x函数内部和外部都引用相同的值,因此请参见修改。相比之下,n在函数内部重新分配后,函数内部和外部引用不同的n


8
“复制”具有误导性。Python没有像C这样的变量。Python中的所有名称都是引用。您不能修改名称,只需将其绑定到另一个对象即可。谈论Python中的可变对象是不可变的,而不是名称。
jfs

1
@JF Sebastian:您的陈述充其量是一种误导。将数字视为参考是没有用的。
皮塔鲁2009年

9
@dysfunctor:数字是对不可变对象的引用。如果您想以其他方式考虑它们,则可以解释很多奇怪的特殊情况。如果您认为它们是不变的,则没有特殊情况。
S.Lott

@ S.Lott:不管引擎盖下发生了什么,Guido van Rossum都花了很多心血来设计Python,以便程序员可以将数字视为...数字。
09年

1
@JF,引用被复制。
哈比人

7

我将重命名变量以减少混乱。 n- > nf nmainx- > 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仍指向的列表。

希望有帮助。:-)


2
“在n的情况下,将复制的值...”-这是错误的,此处没有进行复制(除非您计算引用数)。相反,python使用指向实际对象的“名称”。nf和xf指向nmain和xmain,直到nf = 2,名称nf更改为指向为止2。数字是不可变的,列表是可变的。
Casey Kuball 2012年

2

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

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

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

3
它与可变性无关。如果您愿意这样做,尽管列表是可变x = x + [4]x.append(4),但您也不会在调用方中看到任何更改。它有做的,如果它确实发生突变。
glglgl 2014年

1
OTOH,如果您这样做了,x += [4]x就像发生的一样,是突变的x.append(4),因此呼叫者将看到更改。
PM 2Ring

2

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

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


3
观看“指针副本”的措辞。两个地方都获得对对象的引用。n是对不可变对象的引用;x是对可变对象的引用。
S.Lott

2

如果用完全不同的变量重写函数,然后在它们上调用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。与本文所述相同的基础结构只有不同的标记。


0

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

对比这两个功能

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

def goo(x):
    x = []

现在,当您输入外壳时

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

与此进行比较。

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

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

在第二种情况下,您将cow的地址副本传递给了goo。然后goo继续更改该副本。效果:无。

我称之为粉红色房屋原则。如果您复制住址并告诉画家将该地址的房子粉刷成粉红色,那么您将得到一间粉红色的房子。如果您给画家一个地址的副本,并告诉他将其更改为新地址,则您房屋的地址不变。

这种解释消除了很多混乱。Python通过值传递地址变量存储。


如果您以正确的方式考虑,纯指针传递值与引用传递没有太大区别...
galinette 2014年

你看 如果您纯粹通过引用传递,它将改变其论点。不,Python不是纯粹的引用传递语言。它按值传递引用。
ncmathsadist,2014年

0

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


0

我的一般理解是,任何对象变量(例如列表或字典等)都可以通过其功能进行修改。我认为您无法执行的操作是重新分配参数-即,在可调用函数中通过引用进行分配。

这与许多其他语言一致。

运行以下简短脚本以查看其工作方式:

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

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

-3

我已经无数次修改了答案,并意识到我不必说什么,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,而是语法,等号。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.