为啥函数可以修改调用者感知的某些参数,而不能修改其他参数?

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 -> 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,并且类似地分配一个副本nmainnf.

对于n,复制的值为1。

x 的情况下,复制的值不是文字列表 [0, 1, 2, 3]。它是对该列表的引用xfxmain 指向同一个列表,所以当你修改 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) 发生的情况一样,所以调用者会看到变化。

以上是关于为啥函数可以修改调用者感知的某些参数,而不能修改其他参数?的主要内容,如果未能解决你的问题,请参考以下文章

C#中数组作为参数传递的问题

java中引用传递和值传递的理解

在java中,引用数据不就是一种对象么?为啥在调用函数中不能进行修改数值??

php设计api,只能以echo的形式返回给调用者吗?为啥用return就不能返回数据呢?

封装与扩展性

Python 封装