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

2024-11-22 08:48:00
admin
原创
161
摘要:问题描述:我正在尝试理解 Python 对变量范围的处理方法。在此示例中,为什么 能够f()改变 的值x(如 内所见main()),但不能改变 的值n?def f(n, x): n = 2 x.append(4) print('In f():', n, x) def main(): ...

问题描述:

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

参见:

  • 如何通过引用传递变量?

  • Python 变量是指针吗?或者,它们是什么?


解决方案 1:

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

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 中的名称之间的区别的精美图片。

解决方案 2:

您已经得到了许多答案,并且我大体上同意 JF Sebastian 的观点,但您可能会发现这是一个有用的捷径:

只要您看到,您就会在函数范围内varname =创建一个新的varname名称绑定。之前绑定的任何值都会在此范围内丢失。

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

[请注意,global关键字对第一种情况产生了例外]

解决方案 3:

f实际上并没有改变 的值x(它始终是对列表实例的相同引用)。相反,它改变了此列表的内容。

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

  • n被赋予一个新值。只有函数内部的引用被修改,而函数外部的引用不会被修改。

  • x未分配新值:函数内部和外部的引用均未修改。相反,x被修改。

由于函数内部和外部都x引用同一个值,因此两者都看到了修改。相反,在函数内部重新赋值后,函数n内部和外部引用的值不同。n

解决方案 4:

我将重命名变量以减少混淆。n - > nfnmain。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 ,同样地,还会将nmai的副本分配 给nf

对于n来说,复制的值是 1。

对于x来说,复制的值不是文字列表[0, 1, 2, 3]。它是对该列表的 引用。 xfxmain指向同一个列表,因此当您修改xf时,您也会修改xmain

但是,如果你要写类似这样的内容:

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

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

希望有所帮助。:-)

解决方案 5:

如果用完全不同的变量重写函数,然后我们对其调用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。正如文章所说,只是针对相同底层结构的不同标签。

解决方案 6:

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

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

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

解决方案 7:

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

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

解决方案 8:

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

这与许多其他语言一致。

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

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

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

解决方案 9:

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

解决方案 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 通过值传递变量存储的地址。

解决方案 11:

当您在函数内部传递命令 n​​ = 2 时,它会找到一个内存空间并将其标记为 2。但是如果您调用方法 append,您基本上是引用位置 x(无论该值是什么)并对其执行一些操作。

解决方案 12:

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

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

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

  3. 当您使用“something”=“anything_new”时,您会更改“something”指向的内容。当您使用 .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.append(4) 还是 n.append(4),第二段代码的结果都是相同的。

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

解决方案 13:

请允许我再次编辑。这些概念是我通过 try error 和互联网(主要是 stackoverflow)学习 Python 的经验。其中有错误,也有帮助。

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

当我们这样做时B = A,我们实际上创建了 A 的一个昵称,现在 A 有 2 个名字,A 和 B。当我们调用 B 时,我们实际上是在调用 A。我们创建了一个指向其他变量值的引用,而不是创建一个新的相同值,这就是我们所说的引用。这种想法会导致 2 个问题。

当我们这样做的时候

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 的值的相同地址。

字符串、数字、元组,值和地址的配对永远不能改变,当我们把一个字符串放到某个地址时,它马上就被锁定了,所有修改的结果都会放到其他地址中。

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,而是语法、等号。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1590  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1361  
  信创产品在政府采购中的占比分析随着信息技术的飞速发展以及国家对信息安全重视程度的不断提高,信创产业应运而生并迅速崛起。信创,即信息技术应用创新,旨在实现信息技术领域的自主可控,减少对国外技术的依赖,保障国家信息安全。政府采购作为推动信创产业发展的重要力量,其对信创产品的采购占比情况备受关注。这不仅关系到信创产业的发展前...
信创和国产化的区别   18  
  信创,即信息技术应用创新产业,旨在实现信息技术领域的自主可控,摆脱对国外技术的依赖。近年来,国货国用信创发展势头迅猛,在诸多领域取得了显著成果。这一发展趋势对科技创新产生了深远的推动作用,不仅提升了我国在信息技术领域的自主创新能力,还为经济社会的数字化转型提供了坚实支撑。信创推动核心技术突破信创产业的发展促使企业和科研...
信创工作   18  
  信创技术,即信息技术应用创新产业,旨在实现信息技术领域的自主可控与安全可靠。近年来,信创技术发展迅猛,对中小企业产生了深远的影响,带来了诸多不可忽视的价值。在数字化转型的浪潮中,中小企业面临着激烈的市场竞争和复杂多变的环境,信创技术的出现为它们提供了新的发展机遇和支撑。信创技术对中小企业的影响技术架构变革信创技术促使中...
信创国产化   19  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用