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

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

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

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


当前回答

除了所有关于Python中这些东西的工作原理的精彩解释之外,我看不到任何关于这个问题的简单建议。就像您创建对象和实例一样,处理实例变量并更改它们的Python方式如下:

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

    def Change(self):
        self.variable = 'Changed'

在实例方法中,通常引用self来访问实例属性。在__init__中设置实例属性并在实例方法中读取或更改它们是正常的。这也是为什么将self-als作为第一个参数传递给def Change。

另一种解决方案是创建如下静态方法:

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

    @staticmethod
    def Change(var):
        var = 'Changed'
        return var

其他回答

在这种情况下,方法Change中名为var的变量被分配了对self.variable的引用,并且您立即将字符串分配给var。它不再指向self.variable.下面的代码片段显示了如果您修改了var和self.variaable指向的数据结构(在本例中是一个列表)会发生什么:

>>> class PassByReference:
...     def __init__(self):
...         self.variable = ['Original']
...         self.change(self.variable)
...         print self.variable
...         
...     def change(self, var):
...         var.append('Changed')
... 
>>> q = PassByReference()
['Original', 'Changed']
>>> 

我相信其他人可以进一步澄清这一点。

通过引用传递对象有一个小技巧,即使语言无法实现。它也适用于Java,它是一个列表,只有一项。;-)

class PassByReference:
    def __init__(self, name):
        self.name = name

def changeRef(ref):
    ref[0] = PassByReference('Michael')

obj = PassByReference('Peter')
print obj.name

p = [obj] # A pointer to obj! ;-)
changeRef(p)

print p[0].name # p->name

这是一个丑陋的黑客,但它奏效了-P

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

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

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])

虽然这看起来有点麻烦。

这里的答案有很多见解,但我认为这里没有明确提到另外一点。引用python文档https://docs.python.org/2/faq/programming.html#what-是python中局部和全局变量的规则

“在Python中,仅在函数内部引用的变量是隐式全局的。如果在函数体中的任何位置为变量分配了新值,则假定该变量是局部的。如果变量在函数内部被分配了新的值,则该变量隐式是局部的,您需要将其显式声明为“全局”。虽然一开始有点令人惊讶,但片刻的考虑可以解释这一点。一方面,要求全局分配变量可以防止意外的副作用。另一方面,如果所有全局引用都需要全局引用,那么您将一直使用全局引用。您必须将对内置函数或导入模块组件的每个引用声明为全局引用。这种混乱将破坏全球宣言在确定副作用方面的作用。"

即使在将可变对象传递给函数时,这仍然适用。对我来说,这清楚地解释了分配给对象和在函数中操作对象之间行为差异的原因。

def test(l):
    print "Received", l , id(l)
    l = [0, 0, 0]
    print "Changed to", l, id(l)  # New local object created, breaking link to global l

l= [1,2,3]
print "Original", l, id(l)
test(l)
print "After", l, id(l)

给予:

Original [1, 2, 3] 4454645632
Received [1, 2, 3] 4454645632
Changed to [0, 0, 0] 4474591928
After [1, 2, 3] 4454645632

因此,对未声明为全局的全局变量的赋值将创建一个新的局部对象,并断开与原始对象的链接。

除了所有关于Python中这些东西的工作原理的精彩解释之外,我看不到任何关于这个问题的简单建议。就像您创建对象和实例一样,处理实例变量并更改它们的Python方式如下:

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

    def Change(self):
        self.variable = 'Changed'

在实例方法中,通常引用self来访问实例属性。在__init__中设置实例属性并在实例方法中读取或更改它们是正常的。这也是为什么将self-als作为第一个参数传递给def Change。

另一种解决方案是创建如下静态方法:

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

    @staticmethod
    def Change(var):
        var = 'Changed'
        return var