Python函数的调用是按引用传递的

109

在一些编程语言中,你可以使用特殊保留字 refval 来按引用或值传递参数。但是,在 Python 函数中传递参数时,离开函数后它的值不会被改变。唯一能改变其值的方式是使用保留字 global(或者是我目前的理解)。

示例 1:

k = 2

def foo (n):
     n = n * n     #clarity regarding comment below
     square = n
     return square

j = foo(k)
print j
print k

将展示

>>4
>>2

k保持不变。

在这个例子中,变量n从未被改变。

例子2:

n = 0
def foo():
    global n
    n = n * n
    return n
在这个例子中,变量n被更改。 有没有办法在Python中调用一个函数并告诉Python 参数参数还是引用参数,而不是使用全局变量?

5
我发现了一个非常好的资源,可以帮助你理解Python的调用模型。这篇关于Python调用对象的文章来自effbot网站:http://effbot.org/zone/call-by-object.htm。 - Wilduck
1
你应该了解Python变量、可变和不可变对象。此外,在你的第一个例子中,为什么n会被改变,你使用一个新变量square来存储计算结果。 - Facundo Casco
2
此外,请查看此前的StackOverflow答案。https://dev59.com/vmkv5IYBdhLWcg3w6k_j#10262945 它相当直观地解释了对象引用调用模型。 - Wilduck
1
请参考这个非常有用的解释SO问题:如何通过引用传递变量 - Michael Ohlrogge
我有一个解决方法:通过引用将a传递给function()函数: list=[a]; function(list[0]); 现在list中的数据是一个引用。我没有发布答案,因为这似乎不是正确的方法,但我很想知道正确的方法是什么。 - Supamee
12个回答

219

基本上有三种“函数调用”方式:

  • 按值传递
  • 按引用传递
  • 按对象引用传递

Python是一种按对象引用传递的编程语言。

首先,重要的是要理解一个变量和变量的值(即对象)是两个不同的东西。变量“指向”对象,而变量本身并不是对象。再强调一次:

变量不是对象

例如,在下面的代码行中:

>>> x = []

[]是一个空列表,x是指向空列表的变量,但是x本身不是空列表。

将变量(在上面的例子中为x)看作盒子,而变量([])的“值”是盒子内部的对象。

按对象引用传递(Python中的情况):

这里,“通过值传递对象引用。”

def append_one(li):
    li.append(1)
x = [0]
append_one(x)
print x

这里,语句 x = [0] 创建了一个指向对象 [0] 的变量 x(盒子)。

在函数被调用时,会创建一个新的盒子 li。盒子 li 的内容与盒子 x 的内容相同。两个盒子都包含相同的对象。也就是说,两个变量指向内存中的同一对象。因此,对 li 指向的对象所做的任何更改也将反映在 x 指向的对象上。

总之,以上程序的输出将为:

[0, 1]

注意:

如果在函数中重新分配变量li,那么li将指向内存中的一个单独对象。然而,x仍然将继续指向它之前指向的相同对象。

例如:

def append_one(li):
    li = [0, 1]
x = [0]
append_one(x)
print x

程序的输出将会是:

[0]

按引用传递:

来自调用函数的盒子被传递给被调用的函数。隐含地,盒子的内容(变量的值)被传递给了被调用的函数。因此,在被调用的函数中更改盒子内容的任何变化都将在调用函数中反映出来。

按值传递:

在被调用函数中创建一个新的盒子,并将调用函数中盒子的内容的副本存储到新的盒子中。


4
当采用按引用传递时,函数接收的是对变量内存地址的引用,因此任何对该参数的更改都会影响原始变量的值。例如,C++中的指针和Java中的对象都是以按引用方式传递的。当使用按值传递时,函数接收的是变量的值的副本,而不是变量本身。这意味着任何对函数参数的更改都不会影响原始变量的值。例如,C++中的整数和Java中的基本类型都是以按值方式传递的。 - TJain
6
我仍然不明白这与按引用传递的方式有何不同。这个答案迫切需要一个示例,说明相同的伪代码将如何为所有三种范式产生不同的输出。链接到另一个仅解释按引用传递和按值传递之间区别的答案没有帮助。 - Jess Riedel
难道说“Python中的所有变量都是指针”,“在函数调用中分配给参数的所有变量都被分配给新指针”不是更简单吗?如果你意识到这一点,你会发现Python就像Java一样:它只是始终按值传递指针。请参见Kirk Strauser的答案 - Marco Sulla
@MarcoSulla 不行。因为Python没有指针。这是一个愚蠢的Java主义,这是由于Java有“原始类型”和“引用类型”的事实所必需的。这样的区别不存在。 - juanpa.arrivillaga
@JessRiedel 如果你使用了按引用调用,你可以定义一个函数 def foo(&x): x = 42,我借鉴了 C++ 中的传递引用参数的语法,但是在 Python 中并没有这样的 & 语法。假设在其他任意作用域中,你有 i = 0; foo(i); print(i),那么输出结果将会是 42。但是在 Python 中这是不可能的,因为 Python 不支持按引用调用 - juanpa.arrivillaga
显示剩余8条评论

88

在Python中,您不能更改不可变对象,例如strtuple,但您可以执行以下操作:

def foo(y):
  y[0] = y[0]**2

x = [5]
foo(x)
print x[0]  # prints 25

这是一种奇怪的方式,除非您需要始终对数组中的某些元素进行平方。

请注意,在Python中,您还可以返回多个值,这使得一些传递引用的用例不那么重要:

def foo(x, y):
   return x**2, y**2

a = 2
b = 3
a, b = foo(a, b)  # a == 4; b == 9

当你返回像那样的值时,它们将作为元组返回,然后再进行解包。

编辑: 另一种思考方式是,在Python中您无法显式地通过引用传递变量,但是您可以修改已传入的对象的属性。在我的示例(和其他示例)中,您可以修改传入的列表的成员。但是,您不会能够完全重新分配传入的变量。例如,请查看以下两个代码片段,它们看起来可能执行类似的操作,但最终结果不同:

def clear_a(x):
  x = []

def clear_b(x):
  while x: x.pop()

z = [1,2,3]
clear_a(z) # z will not be changed
clear_b(z) # z will be emptied

2
看到程序的结果会很有帮助。例如,'print x[0]' 返回 25 吗?(是的,但应该显示出来。) - Ray Salemi
12
@alexey 给 clear_a 函数传递了一个对 z 的引用,并将该引用存储在 x 中。然后它立即更改 x 以引用另一个数组。 原始的 z 数组在 clear_a 的范围内被遗忘,但实际上并没有被更改。 当返回到全局范围时,它仍保持不变。与此形成对比的是 clear_b,它接收 z 的引用,然后直接对其进行操作,而不创建新数组或以其他方式指向不同的东西。 - dave mankoff
t= threading.Thread(target=module2.some_method, \ args=(11,1,run_thread)) --do something - and when you want to sop run_thread[0] =False``` - Alex Punnen
Python中没有原始类型,不像Java。所有类型都是实例对象。 - Marco Sulla
1
请注意,在Python中,您还可以返回多个值,从而使一些按引用传递的用例变得不那么重要。但并非总是如此,有时按引用传递与内存管理有关,而不仅仅是改变参数值。有时您不希望在程序内部复制参数,特别是如果您的内存不足。 - user3103059
显示剩余2条评论

30

好的,我来试试。Python按对象引用传递,这与你通常所想的“按引用”或“按值”不同。看这个例子:

def foo(x):
    print x

bar = 'some value'
foo(bar)

所以你正在创建一个值为“some value”的字符串对象,并将其“绑定”到名为bar的变量。在C语言中,这类似于bar是指向“some value”的指针。

当你调用foo(bar)时,你传递的不是bar本身,而是bar的值:指向“some value”的指针。此时,有两个指向同一字符串对象的“指针”。

现在将其与以下内容进行比较:

def foo(x):
    x = 'another value'
    print x

bar = 'some value'
foo(bar)

这就是区别所在。在这行中:

x = 'another value'

实际上,您并没有改变x的内容。事实上,这甚至是不可能的。相反,您正在创建一个新的字符串对象,其值为“另一个值”。那个赋值运算符?它并不是在说“用新值覆盖x指向的东西”。它是在说“更新x以指向新对象”。在那一行之后,有两个字符串对象:“一些值”(由bar指向)和“另一个值”(由x指向)。

这并不笨拙。当您理解它的工作原理时,它是一个非常优雅、高效的系统。


3
如果我 想让 函数改变我的变量内容怎么办? 我理解这样做没有办法,是吗? - aquirdturtle
1
如果对象是可变的,您可以更改_name_指向的对象,例如通过附加到列表。Python没有语义来表示“更改调用我的作用域的命名空间,以便参数列表中的名称现在指向与调用我的时候不同的东西”。 - Kirk Strauser
2
有效地说,我的问题是是否有一种简单的方法让 Python 不创建指向同一对象的新指针,而是使用原始指针。我认为没有办法做到这一点。从其他答案中可以看出,实现传递引用的正常解决方法就是返回更多参数。如果是这样的话,我认为通过 a、b、c、d、e=foo(a、b、c、d、e) 比只使用 foo(a、b、c、d、e) 更麻烦一些,这个观点是有一定道理的。 - aquirdturtle
2
@aquiradturtle,这与创建新指针无关。您必须返回一个新对象,因为“str”、 “tuple”和其他对象是不可变的。如果传递了可变对象,例如“list”,则可以在函数内部更改它,而无需将其返回,因为所有函数参数都是指向传递的同一对象的指针。 - Marco Sulla
@KirkStrauser 我认为解释Python语义最好的是Ned Batchelder的文章。关于Python名称和值的事实与神话。请注意,它提到了指针,但实际上并不依赖指针的概念。 - juanpa.arrivillaga
显示剩余5条评论

28
希望以下描述能够很好地总结:

这里有两件事情需要考虑 - 变量和对象。

  1. 如果你传递一个变量,那么它是按值传递的,这意味着在函数内部对变量所做的更改仅在该函数中局部有效,因此不会全局反映。这更像是C语言的行为。

示例:

def changeval( myvar ):
   myvar = 20; 
   print "values inside the function: ", myvar
   return

myvar = 10;
changeval( myvar );
print "values outside the function: ", myvar

输出:

values inside the function:  20 
values outside the function:  10
  1. 如果您将变量打包在可变对象(比如列表)中传递,那么只要未对该对象进行重新赋值,对该对象所做的更改将在全局范围内反映。

示例:

def changelist( mylist ):
   mylist2=['a'];
   mylist.append(mylist2);
   print "values inside the function: ", mylist
   return

mylist = [1,2,3];
changelist( mylist );
print "values outside the function: ", mylist
values inside the function:  [1, 2, 3, ['a']]
values outside the function:  [1, 2, 3, ['a']]
  1. 现在考虑对象被重新赋值的情况。在这种情况下,对象引用一个新的内存位置,它是局部于此发生的函数的,并且因此不会在全局范围内反映。

示例:

def changelist( mylist ):
   mylist=['a'];
   print "values inside the function: ", mylist
   return

mylist = [1,2,3];
changelist( mylist );
print "values outside the function: ", mylist
values inside the function:  ['a']
values outside the function:  [1, 2, 3]

2
这是最好的总结! ^^^ 最佳总结在此 ^^^ 每个人都应该阅读这个答案!1. 传递简单变量2. 传递被修改的对象引用3. 传递在函数中被重新分配的对象引用 - gaoithe
5
这个回答有些混淆。Python 不会以不同的方式传递不同类型的值:Python 中的所有函数参数都通过赋值来接收它们的值,并且这种赋值行为与语言中任何其他地方的赋值行为完全相同。(“虽然一开始可能不太明显,除非你是荷兰人。”) - Daniel Pryden

10

从技术上讲,Python 不通过值传递参数:全部都是通过引用传递的。但是……由于 Python 有两种对象类型:可变和不可变,所以会出现下面这种情况:

  • 不可变参数实际上是按值传递的:字符串、整数、元组都是不可变对象类型。虽然它们在技术上是“按引用”传递的(如同所有参数),但由于你无法在函数内部原地进行更改,因此看起来/表现得就像按值传递一样。

  • 可变参数实际上是按引用传递的:列表或字典是通过指针传递的。任何在函数内部的原地更改(例如添加或删除)都会影响原始对象。

这就是 Python 的设计方式:没有副本,所有对象都是通过引用传递的。你可以显式地传递一个副本。

def sort(array):
    # do sort
    return array

data = [1, 2, 3]
sort(data[:]) # here you passed a copy

我想提到的最后一点是,函数有其自己的作用域。

def do_any_stuff_to_these_objects(a, b): 
    a = a * 2 
    del b['last_name']

number = 1 # immutable
hashmap = {'first_name' : 'john', 'last_name': 'legend'} # mutable
do_any_stuff_to_these_objects(number, hashmap) 
print(number) # 1 , oh  it should be 2 ! no a is changed inisde the function scope
print(hashmap) # {'first_name': 'john'}

1
不可变参数实际上是按值传递的:字符串、整数、元组都是按引用传递的。这句话有自相矛盾之处。 - eral
@eral,如果你了解全部背景,它就不会自相矛盾。我编辑过以使其更清晰。 - Watki02

10
Python既不是按值传递也不是按引用传递。它更像是“对象引用按值传递”,如此处所述:
  1. Here's why it's not pass-by-value. Because

    def append(list):
        list.append(1)
    
    list = [0]
    reassign(list)
    append(list)
    

返回 [0,1] 表明某种引用被明确地传递,因为按值传递不允许函数修改父级作用域

看起来像是按引用传递,对吧?不是的。

  1. Here's why it's not pass-by-reference. Because

    def reassign(list):
      list = [0, 1]
    
    list = [0]
    reassign(list)
    print list
    

返回 [0] 表明当 list 被重新赋值时原始引用已被销毁。使用传引用方式会返回 [0,1]。

如需了解更多信息,请查看 此处:

如果你希望函数不影响外部作用域,需要复制输入参数以创建新对象。

from copy import copy

def append(list):
  list2 = copy(list)
  list2.append(1)
  print list2

list = [0]
append(list)
print list

3

所以这是一个微妙的问题,因为虽然Python只通过值传递变量,但Python中的每个变量都是一个引用。如果您想要能够使用函数调用更改您的值,您需要的是可变对象。例如:

l = [0]

def set_3(x):
    x[0] = 3

set_3(l)
print(l[0])

在上述代码中,该函数修改了List对象的内容(该对象是可变的),因此输出结果为3而不是0。
我写这个答案只是为了说明Python中“按值传递”的含义。上述代码风格不好,如果你真的想改变值,应该编写一个类并在该类中调用方法,就像MPX建议的那样。

这个问题的答案只回答了一半,那么我该如何编写相同的 set_3(x) 函数,以便它不改变 l 的值,而是返回一个已更改的新列表? - Timothy Lawman
只需创建一个新列表并进行修改:y = [a for a in x]; y[0] = 3; return y - Isaac
整个过程涉及到列表和复制列表,这在某种程度上可以回答问题,但是我如何在不将int转换为列表的情况下完成它呢?或者这是我们所拥有的最好方法吗? - Timothy Lawman
你不能修改像int这样的原始参数。你必须将它放入某种容器中。我的例子将其放入了一个列表中。你也可以将其放入一个类或字典中。但实际上,你应该在函数外使用类似于x = foo(x)的方式重新分配它。 - Isaac

1
考虑变量是一个盒子,它所指向的值是盒子里的“东西”:
1. 引用传递:函数共享同一个盒子,因此里面的东西也是一样的。
2. 值传递:函数创建一个新的盒子,是旧盒子的副本,包括里面的任何东西。例如 Java - 函数会创建盒子和里面的东西的一个副本,可以是基本类型或对象的引用。(注意,在新盒子中复制的引用和原始引用仍然指向相同的对象,这里引用是盒子里的内容,而不是它指向的对象)
3. 对象引用传递:函数创建一个盒子,但其中包含的是与初始盒子相同的东西。因此在 Python 中:
a)如果盒子内的东西是可变的,则所做的更改将反映在原始盒子中(例如列表)。

b) 如果这个东西是不可变的(比如 Python 字符串和数值类型),那么函数内部的盒子将会持有相同的东西,直到你尝试改变它的值。一旦改变了,函数盒子中的东西就与原始的完全不同了。因此,该盒子的 id() 现在将给出它所包含的新事物的标识。


0

给出的答案是

def set_4(x):
   y = []
   for i in x:
       y.append(i)
   y[0] = 4
   return y

l = [0]

def set_3(x):
     x[0] = 3

set_3(l)
print(l[0])

目前为止,这是最好的答案,因为它做到了问题所说的。然而,与VB或Pascal相比,它似乎非常笨拙。这是我们拥有的最好方法吗?

不仅笨拙,而且涉及以某种方式手动改变原始参数,例如通过将原始参数更改为列表:或将其复制到另一个列表而不仅仅是说:“使用此参数作为值”或“使用此参数作为引用”。简单的答案可能是没有保留字来实现这一点,但这些都是很好的解决方法吗?


0
class demoClass:
    x = 4
    y = 3
foo1 = demoClass()
foo1.x = 2
foo2 = demoClass()
foo2.y = 5
def mySquare(myObj):
    myObj.x = myObj.x**2
    myObj.y = myObj.y**2
print('foo1.x =', foo1.x)
print('foo1.y =', foo1.y)
print('foo2.x =', foo2.x)
print('foo2.y =', foo2.y)
mySquare(foo1)
mySquare(foo2)
print('After square:')
print('foo1.x =', foo1.x)
print('foo1.y =', foo1.y)
print('foo2.x =', foo2.x)
print('foo2.y =', foo2.y)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接