关于python:如何通过引用传递变量?

How do I pass a variable by reference?

python文档似乎不清楚参数是通过引用还是通过值传递的,下面的代码生成的值"original"不变。

1
2
3
4
5
6
7
8
class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change(self.variable)
        print(self.variable)

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

我可以通过实际引用传递变量吗?


参数通过赋值传递。这背后的理由是双重的:

  • 传入的参数实际上是对对象的引用(但引用是按值传递的)
  • 有些数据类型是可变的,但另一些数据类型不是可变的
  • 所以:

    • 如果你将一个可变的对象传递给一个方法,这个方法会得到一个对同一个对象的引用,你可以让它发生变化,让你的心高兴,但是如果你在方法中重新绑定引用,外部的作用域将对它一无所知,完成后,外部的引用仍然会指向原始的对象。

    • 如果将不可变的对象传递给方法,则仍然无法重新绑定外部引用,甚至无法改变对象。

    为了更清楚地说明这一点,我们来举几个例子。

    列表-可变类型

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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)

    输出:

    1
    2
    3
    4
    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的引用,而不是它的副本,所以我们可以使用可变列表方法来更改它,并将更改反映在外部范围中。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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)

    输出:

    1
    2
    3
    4
    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_listouter_list参考文献的副本,我们让the_list指向一个新的列表,但是没有办法改变outer_list指出的地方。

    字符串-不可变类型

    它是不可变的,所以我们无法更改字符串的内容。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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)

    输出:

    1
    2
    3
    4
    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

    同样,由于the_string参数是通过值传递的,因此为它分配一个新的字符串对方法之外的代码看不到任何影响。the_stringouter_string参考文献的副本,我们让the_string指向一个新字符串,但没有办法改变outer_string所指的位置。

    我希望这能把事情弄清楚一点。

    edit:注意到这并不能回答@david最初提出的问题,"我是否可以通过实际引用传递变量?"我们来研究一下。

    我们怎么解决这个问题?

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

    1
    2
    3
    4
    5
    6
    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)

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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中的变量的误解。如果你习惯了大多数的传统语言,你就有了一个心理模型,可以按照以下顺序进行:

    1
    2
    a = 1
    a = 2

    您认为a是存储值1的内存位置,然后更新以存储值2。这不是Python中的工作方式。相反,a开始作为对值为1的对象的引用,然后重新分配为对值为2的对象的引用。即使a不再引用第一个对象,这两个对象也可能继续共存;事实上,它们可以由程序中的任何其他引用共享。

    当使用参数调用函数时,将创建一个引用传入对象的新引用。这与函数调用中使用的引用不同,因此无法更新该引用并使其引用新对象。在您的示例中:

    1
    2
    3
    4
    5
    6
    def __init__(self):
        self.variable = 'Original'
        self.Change(self.variable)

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

    self.variable是对字符串对象'Original'的引用。当您调用Change时,您将创建对该对象的第二个引用var。在函数内部,您将引用var重新分配给另一个字符串对象'Changed',但引用self.variable是独立的,不会更改。

    唯一的方法是传递一个可变的对象。因为这两个引用引用同一个对象,所以对该对象的任何更改都会反映在这两个地方。

    1
    2
    3
    4
    5
    6
    def __init__(self):        
        self.variable = ['Original']
        self.Change(self.variable)

    def Change(self, var):
        var[0] = 'Changed'


    我发现其他的答案相当长和复杂,所以我创建了这个简单的图来解释Python处理变量和参数的方式。enter image description here


    它既不是传递值也不是传递引用-它是由对象调用的。见Fredrik Lundh:

    http://effbot.org/zone/call-by-object.htm(按对象调用)

    这是一个重要的引述:

    "...variables [names] are not objects; they cannot be denoted by other variables or referred to by objects."

    在您的示例中,当调用Change方法时--为其创建一个命名空间;并且var在该命名空间中成为字符串对象'Original'的名称。然后,该对象在两个名称空间中有一个名称。接下来,var = 'Changed'var绑定到一个新的字符串对象,因此该方法的命名空间忘记了'Original'。最后,这个名称空间被遗忘了,字符串'Changed'也被遗忘了。


    想想那些通过赋值而不是通过引用/值传递的东西。这样,只要你理解在正常作业中发生的事情,一切都很清楚。

    因此,当将列表传递给函数/方法时,该列表被分配给参数名。附加到列表将导致列表被修改。在函数内重新分配列表不会更改原始列表,因为:

    1
    2
    3
    4
    5
    a = [1, 2, 3]
    b = a
    b.append(4)
    b = ['a', 'b']
    print a, b      # prints [1, 2, 3, 4] ['a', 'b']

    由于不能修改不可变类型,它们看起来像是按值传递的——将int传递给函数意味着将int分配给函数参数。您只能重新分配它,但它不会改变原始变量的值。


    effbot(又名fredrik lundh)将python的变量传递样式描述为按对象调用:http://effbot.org/zone/call-by-object.htm

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

    • 当进行诸如x = 1000这样的赋值时,将创建一个字典条目,该条目将当前命名空间中的字符串"x"映射到指向包含1000的整数对象的指针。

    • 使用x = 2000更新"x"时,将创建一个新的整数对象,并更新字典以指向新对象。旧的一千个对象是不变的(可能还活着,也可能不活着,这取决于是否有其他的对象)。

    • 当您执行新的赋值(如y = x时,将创建一个新的字典条目"y",该条目指向与"x"条目相同的对象。

    • 像字符串和整数这样的对象是不可变的。这仅仅意味着在创建对象之后,没有方法可以更改它。例如,一旦创建了整数对象1000,它将永远不会改变。数学是通过创建新的整数对象来完成的。

    • 像列表这样的对象是可变的。这意味着可以通过指向对象的任何内容来更改对象的内容。例如,x = []; y = x; x.append(10); print y将打印[10]。已创建空列表。"x"和"y"都指向同一列表。append方法改变(更新)列表对象(如向数据库添加记录),结果对"x"和"y"都可见(就像数据库更新对该数据库的每个连接都可见一样)。

    希望你能澄清这个问题。


    从技术上讲,python总是使用pass-by引用值。我要重复我的另一个答案来支持我的陈述。

    python总是使用传递引用值。没有例外。任何变量赋值都意味着复制引用值。也不例外。任何变量都是绑定到引用值的名称。总是。

    可以将引用值视为目标对象的地址。地址在使用时自动取消引用。这样,使用引用值时,您似乎直接使用目标对象。但是在两者之间总是有一个参照物,要跳到目标上还要多走一步。

    下面的示例证明了Python使用了按引用传递:

    Illustrated example of passing the argument

    如果参数是按值传递的,则不能修改外部lst。绿色是目标对象(黑色是存储在其中的值,红色是对象类型),黄色是内存,其中的引用值——绘制为箭头。蓝色实心箭头是传递给函数的参考值(通过蓝色虚线箭头路径)。难看的深黄色是内部词典。(实际上也可以画成绿色椭圆。颜色和形状只表示它是内部的。)

    您可以使用id()内置函数来了解参考值是什么(即目标对象的地址)。

    在编译语言中,变量是能够捕获类型值的内存空间。在Python中,变量是绑定到保存目标对象引用值的引用变量的名称(在内部捕获为字符串)。变量的名称是内部字典中的键,该字典项的值部分存储对目标的引用值。

    引用值隐藏在python中。没有任何用于存储引用值的显式用户类型。但是,可以使用一个列表元素(或任何其他合适容器类型中的元素)作为引用变量,因为所有容器都将元素存储为对目标对象的引用。换句话说,元素实际上不包含在容器中——只有对元素的引用才是。


    我通常使用的一个简单技巧是将它包装在一个列表中:

    1
    2
    3
    4
    5
    6
    def Change(self, var):
        var[0] = 'Changed'

    variable = ['Original']
    self.Change(variable)      
    print variable[0]

    (是的,我知道这很不方便,但有时做起来很简单。)


    (编辑-布莱尔更新了他广受欢迎的答案,使其准确无误)

    我认为重要的是要注意到,目前获得最多选票的职位(布莱尔·康拉德),虽然其结果是正确的,但具有误导性,并且根据其定义是不正确的。虽然有许多语言(如C)允许用户通过引用或传递值,但python不是其中之一。

    大卫·库尔纳波的回答指向了真实的答案,并解释了为什么布莱尔·康拉德的文章中的行为似乎是正确的,而定义却不正确。

    如果python是传递值,那么所有语言都是传递值,因为必须发送一些数据(无论是"值"还是"引用")。然而,这并不意味着在C程序员会想到的意义上,python是传递值。

    如果你想这样做,布莱尔·康拉德的回答很好。但是如果你想知道为什么python既不是传递值,也不是传递引用,那么请阅读大卫·库纳波的答案。


    python中没有变量

    理解参数传递的关键是停止思考"变量"。在python中有名称和对象,它们在一起看起来像变量,但总是区分这三个变量是有用的。

  • python有名称和对象。
  • 赋值将名称绑定到对象。
  • 将参数传递到函数中也会将名称(函数的参数名称)绑定到对象。
  • 这就是一切。易变性与这个问题无关。

    例子:

    1
    a = 1

    这会将名称a绑定到一个包含值1的integer类型的对象。

    1
    b = x

    这会将名称b绑定到名称x当前绑定到的同一对象。此后,名称b与名称x不再有任何关系。

    请参见Python3语言参考中的第3.1和4.2节。

    因此,在问题中所示的代码中,语句self.Change(self.variable)将名称var(在函数Change的范围内)绑定到持有值'Original'的对象,而赋值var = 'Changed'(在函数Change的主体中)再次将相同的名称分配给另一个对象好吧,但可能是完全不同的东西)。


    你得到了一些很好的答案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    x = [ 2, 4, 4, 5, 5 ]
    print x  # 2, 4, 4, 5, 5

    def go( li ) :
      li = [ 5, 6, 7, 8 ]  # re-assigning what li POINTS TO, does not
      # change the value of the ORIGINAL variable x

    go( x )
    print x  # 2, 4, 4, 5, 5  [ STILL! ]


    raw_input( 'press any key to continue' )


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    >>> 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']
    >>>

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


    Python的PASS分配方案与C++的参考参数选项不完全相同,但实际上与C语言(和其他语言)的参数传递模型非常类似:

    • 不可变参数实际上是"按值"传递的。像整数和字符串这样的对象是由对象引用传递的,而不是通过复制传递的,但是由于无论如何都不能在适当的位置更改不可变对象,因此效果很像复制。
    • 可变参数通过"指针"有效传递。对象如列表字典也通过对象引用传递,这类似于C将数组作为指针传递。可以在函数中就地更改可变对象,很像C数组。

    正如您所说,您需要有一个可变的对象,但让我建议您检查全局变量,因为它们可以帮助您甚至解决此类问题!

    http://docs.python.org/3/faq/programming.html python中局部变量和全局变量的规则是什么?

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    >>> def x(y):
    ...     global z
    ...     z = y
    ...

    >>> x
    <function x at 0x00000000020E1730>
    >>> y
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    NameError: name 'y' is not defined
    >>> z
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    NameError: name 'z' is not defined

    >>> x(2)
    >>> x
    <function x at 0x00000000020E1730>
    >>> y
    Traceback (most recent call last):
      File"<stdin>", line 1, in <module>
    NameError: name 'y' is not defined
    >>> z
    2


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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)

    给予:

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

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


    下面是对Python中使用的pass by object概念的简单(我希望)解释。每当您将对象传递给函数时,对象本身就被传递(Python中的对象实际上是您在其他编程语言中调用的值),而不是对该对象的引用。换句话说,当你打电话时:

    1
    2
    3
    4
    5
    def change_me(list):
       list = [1, 2, 3]

    my_list = [0, 1]
    change_me(my_list)

    正在传递实际对象-[0,1](在其他编程语言中称为值)。因此,实际上,函数change_me将尝试执行如下操作:

    1
    [0, 1] = [1, 2, 3]

    这显然不会改变传递给函数的对象。如果函数如下所示:

    1
    2
    def change_me(list):
       list.append(2)

    然后调用将导致:

    1
    [0, 1].append(2)

    这显然会改变物体。这个答案解释得很好。


    除了所有关于这些东西如何在Python中工作的很好的解释之外,我没有看到一个简单的关于这个问题的建议。就像创建对象和实例一样,处理实例变量和更改实例变量的方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    class PassByReference:
        def __init__(self):
            self.variable = 'Original'
            self.Change()
            print self.variable

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

    在实例方法中,通常使用self访问实例属性。在__init__中设置实例属性并在实例方法中读取或更改这些属性是正常的。这也是为什么你把self作为第一个论点传给def Change的原因。

    另一种解决方案是创建这样的静态方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    有一个小技巧可以通过引用传递一个对象,即使语言不能使之成为可能。它也在Java中工作,它是带有一个项目的列表。;-)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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


    我使用以下方法快速地将两个Fortran代码转换为python。是的,当最初的问题被提出时,它不是通过引用传递的,但在某些情况下是一个简单的工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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


    虽然pass-by引用不适合python,而且很少使用,但有一些解决方法可以实际工作,以使对象当前分配给局部变量,甚至从被调用函数内部重新分配局部变量。

    其基本思想是拥有一个可以进行访问的函数,并且可以作为对象传递到其他函数或存储在类中。

    一种方法是在包装函数中使用global(用于全局变量)或nonlocal(用于函数中的局部变量)。

    1
    2
    3
    4
    5
    6
    7
    8
    def change(wrapper):
        wrapper(7)

    x = 5
    def setter(val):
        global x
        x = val
    print(x)

    同样的思想也适用于读取和使用一个变量。

    对于只是阅读,甚至还有一种更短的方法来使用lambda: x,它返回一个可调用的,当被调用时返回当前的x值。这有点像过去语言中使用的"按名称调用"。

    传递3个包装器来访问变量有点难,因此可以将这些包装器包装到具有代理属性的类中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class ByRef:
        def __init__(self, r, w, d):
            self._read = r
            self._write = w
            self._delete = d
        def set(self, val):
            self._write(val)
        def get(self):
            return self._read()
        def remove(self):
            self._delete()
        wrapped = property(get, set, remove)

    # left as an exercise for the reader: define set, get, remove as local functions using global / nonlocal
    r = ByRef(get, set, remove)
    r.wrapped = 15

    pythons的"反射"支持使获得一个对象成为可能,该对象能够在给定范围内重新分配名称/变量,而无需在该范围内显式定义函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class ByRef:
        def __init__(self, locs, name):
            self._locs = locs
            self._name = name
        def set(self, val):
            self._locs[self._name] = val
        def get(self):
            return self._locs[self._name]
        def remove(self):
            del self._locs[self._name]
        wrapped = property(get, set, remove)

    def change(x):
        x.wrapped = 7

    def test_me():
        x = 6
        print(x)
        change(ByRef(locals(),"x"))
        print(x)

    在这里,ByRef类包装了一个字典访问。因此,对wrapped的属性访问被转换为传递字典中的项目访问。通过传递内置的locals的结果和局部变量的名称,这最终会访问局部变量。从3.5开始的python文档建议更改字典可能不起作用,但它似乎对我有用。


    考虑到python处理值和引用这些值的方式,引用任意实例属性的唯一方法是按名称:

    1
    2
    3
    4
    5
    6
    7
    8
    class PassByReferenceIsh:
        def __init__(self):
            self.variable = 'Original'
            self.change('variable')
            print self.variable

        def change(self, var):
            self.__dict__[var] = 'Changed'

    当然,在真正的代码中,您可以在dict查找中添加错误检查。


    Python中的PASS引用与C++/Java中的按引用传递概念有很大的不同。

    • Java:通过引用传递所有内容,因此调用函数中的参数所做的所有更改都是调用方可见的。
    • C++:允许通过引用或通过值传递。如果参数是通过引用传递的,则可以根据参数是否作为常量传递来修改它。但是,无论常量与否,参数维护对对象的引用,并且不能将引用分配给所调用函数中的其他对象。
    • Python:python是"pass-by-object-reference",其中常有这样的说法:"object-references是按值传递的。"[此处阅读]1。调用者和函数都引用同一个对象,但函数中的参数是一个新变量,它只在调用者中保存对象的副本。像C++一样,一个参数可以在函数中被修改或不被修改,这取决于传递的对象的类型。在被调用函数中不能修改不可变的对象类型,而可变的对象可以更新或重新初始化。更新或重新分配/重新初始化可变变量之间的一个重要区别是,更新后的值会反映回被调用的函数中,而重新初始化的值则不会。将新对象分配给可变变量的范围是Python中函数的局部范围。@blair conrad提供的例子很好地理解了这一点。

    由于您的示例恰好是面向对象的,因此您可以进行以下更改以获得类似的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class PassByReference:
        def __init__(self):
            self.variable = 'Original'
            self.change('variable')
            print(self.variable)

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

    # o.variable will equal 'Changed'
    o = PassByReference()
    assert o.variable == 'Changed'


    您只能使用空类作为实例来存储引用对象,因为内部对象属性存储在实例字典中。请参见示例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class RefsObj(object):
       "A class which helps to create references to variables."
        pass

    ...

    # an example of usage
    def change_ref_var(ref_obj):
        ref_obj.val = 24

    ref_obj = RefsObj()
    ref_obj.val = 1
    print(ref_obj.val) # or print ref_obj.val for python2
    change_ref_var(ref_obj)
    print(ref_obj.val)