参数是通过引用传递还是通过值传递?如何通过引用传递,以便下面的代码输出“Changed”而不是“Original”?

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change(self.variable)
        print(self.variable)

    def change(self, var):
        var = 'Changed'

另请参阅:为什么函数可以修改调用者感知的某些参数,而不能修改其他参数?


当前回答

参数通过赋值传递。这背后的理由有两个:

传入的参数实际上是对对象的引用(但引用是按值传递的)一些数据类型是可变的,但其他数据类型不是

So:

如果您将一个可变对象传递给一个方法,该方法将获得对同一对象的引用,您可以根据自己的喜好对其进行变异,但如果您在方法中重新绑定引用,外部作用域将对此一无所知,完成后,外部引用仍将指向原始对象。如果将不可变对象传递给方法,则仍然无法重新绑定外部引用,甚至无法更改对象。

为了更加清楚,让我们举几个例子。

列表-可变类型

让我们尝试修改传递给方法的列表:

def try_to_change_list_contents(the_list):
    print('got', the_list)
    the_list.append('four')
    print('changed to', the_list)

outer_list = ['one', 'two', 'three']

print('before, outer_list =', outer_list)
try_to_change_list_contents(outer_list)
print('after, outer_list =', outer_list)

输出:

before, outer_list = ['one', 'two', 'three']
got ['one', 'two', 'three']
changed to ['one', 'two', 'three', 'four']
after, outer_list = ['one', 'two', 'three', 'four']

由于传入的参数是outer_list的引用,而不是它的副本,因此我们可以使用mutating list方法来更改它,并将更改反映在外部范围中。

现在,让我们看看当我们试图更改作为参数传入的引用时会发生什么:

def try_to_change_list_reference(the_list):
    print('got', the_list)
    the_list = ['and', 'we', 'can', 'not', 'lie']
    print('set to', the_list)

outer_list = ['we', 'like', 'proper', 'English']

print('before, outer_list =', outer_list)
try_to_change_list_reference(outer_list)
print('after, outer_list =', outer_list)

输出:

before, outer_list = ['we', 'like', 'proper', 'English']
got ['we', 'like', 'proper', 'English']
set to ['and', 'we', 'can', 'not', 'lie']
after, outer_list = ['we', 'like', 'proper', 'English']

由于the_list参数是按值传递的,因此为其分配一个新的列表不会对方法外部的代码产生任何影响。The_list是outer_list引用的副本,我们让_list指向一个新列表,但无法更改outer_list指向的位置。

字符串-不可变类型

它是不可变的,因此我们无法更改字符串的内容

现在,让我们尝试更改引用

def try_to_change_string_reference(the_string):
    print('got', the_string)
    the_string = 'In a kingdom by the sea'
    print('set to', the_string)

outer_string = 'It was many and many a year ago'

print('before, outer_string =', outer_string)
try_to_change_string_reference(outer_string)
print('after, outer_string =', outer_string)

输出:

before, outer_string = It was many and many a year ago
got It was many and many a year ago
set to In a kingdom by the sea
after, outer_string = It was many and many a year ago

同样,由于该_string参数是按值传递的,因此为其分配一个新字符串不会对方法外部的代码产生任何影响。The_string是outer_string引用的副本,我们让_string指向一个新字符串,但无法更改outer_string指向的位置。

我希望这能稍微澄清一下。

编辑:有人指出,这并不能回答@David最初提出的问题,“我能做些什么来通过实际引用传递变量吗?”。让我们继续努力。

我们如何避免这种情况?

正如@Andrea的回答所示,您可以返回新值。这不会改变传递信息的方式,但会让您获得想要的信息:

def return_a_whole_new_string(the_string):
    new_string = something_to_do_with_the_old_string(the_string)
    return new_string

# then you could call it like
my_string = return_a_whole_new_string(my_string)

如果您真的想避免使用返回值,可以创建一个类来保存值并将其传递到函数中,或者使用现有的类,如列表:

def use_a_wrapper_to_simulate_pass_by_reference(stuff_to_change):
    new_string = something_to_do_with_the_old_string(stuff_to_change[0])
    stuff_to_change[0] = new_string

# then you could call it like
wrapper = [my_string]
use_a_wrapper_to_simulate_pass_by_reference(wrapper)

do_something_with(wrapper[0])

虽然这看起来有点麻烦。

其他回答

Effbot(又名Fredrik Lundh)将Python的变量传递风格描述为对象调用:http://effbot.org/zone/call-by-object.htm

对象在堆上分配,指向它们的指针可以在任何地方传递。

当您进行赋值(如x=1000)时,将创建一个字典条目,将当前名称空间中的字符串“x”映射到包含1000的整数对象的指针。当您使用x=2000更新“x”时,将创建一个新的整数对象,并更新字典以指向新对象。旧的一千个对象是不变的(可能还活着,也可能不活着,这取决于是否有任何其他对象指向该对象)。当您执行新的赋值(如y=x)时,将创建一个新的字典条目“y”,该条目指向与“x”条目相同的对象。字符串和整数等对象是不可变的。这仅仅意味着在创建对象之后,没有任何方法可以更改对象。例如,一旦整数对象一千被创建,它将永远不会改变。数学是通过创建新的整数对象来完成的。像列表这样的对象是可变的。这意味着对象的内容可以通过指向该对象的任何对象进行更改。例如,x=[];y=x;x.append(10);打印y将打印[10]。已创建空列表。“x”和“y”都指向同一列表。append方法变异(更新)列表对象(如向数据库添加记录),结果对“x”和“y”都可见(正如数据库更新对数据库的每个连接都可见)。

希望这能为您澄清问题。

我使用以下方法将一些Fortran代码快速转换为Python。的确,它不像最初提出的问题那样通过引用传递,但在某些情况下是一个简单的解决方案。

a=0
b=0
c=0
def myfunc(a,b,c):
    a=1
    b=2
    c=3
    return a,b,c

a,b,c = myfunc(a,b,c)
print a,b,c

或者,你可以使用看起来像这样的ctypes

import ctypes

def f(a):
    a.value=2398 ## resign the value in a function

a = ctypes.c_int(0)
print("pre f", a)
f(a)
print("post f", a)

因为a是一个c整数,而不是python整数,并且通过引用传递。然而,你必须小心,因为可能会发生奇怪的事情,因此不建议

Python中的“通过引用”与C++/Java中的“引用传递”概念截然不同。

Java&C#:基元类型(包括字符串)通过值传递(副本),引用类型通过引用传递(地址副本),因此调用方可以看到调用函数中参数的所有更改。C++:允许通过引用或通过值传递。如果参数是通过引用传递的,则可以根据参数是否作为常量传递来修改它。但是,无论是否为常量,参数都保持对对象的引用,并且不能将引用指定为指向所调用函数中的其他对象。蟒蛇:Python是“按对象引用传递”,人们常说:“对象引用是按值传递的。”。调用者和函数都引用同一个对象,但函数中的参数是一个新变量,它只是在调用者中保存对象的副本。与C++一样,参数可以在函数中修改或不修改-这取决于传递的对象类型。如;不可变对象类型不能在调用的函数中修改,而可变对象可以更新或重新初始化。更新或重新分配/重新初始化可变变量之间的一个关键区别是,更新的值会在调用的函数中反映出来,而重新初始化的值则不会。将新对象分配给可变变量的作用域是python中函数的本地作用域。@blair conrad提供的例子很好地理解了这一点。

Python的传递赋值方案与C++的引用参数选项并不完全相同,但实际上它与C语言(以及其他语言)的参数传递模型非常相似:

不可变的参数实际上是“按值”传递的。整数和字符串等对象是通过对象引用传递的,而不是通过复制传递的,但因为无论如何都不能在原地更改不可变的对象,所以效果很像复制。可变参数是“通过指针”有效传递的字典也通过对象引用传递,这与C的方式类似传递数组作为指针,可变对象可以在函数中的适当位置改变,很像C阵列。