如何通过引用传递变量?

Python 文档似乎尚不清楚参数是通过引用还是通过值传递,并且以下代码产生不变的值 “原始”

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

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

我可以做些什么来通过实际引用传递变量吗?

答案

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

  1. 传入的参数实际上是对象的引用(但引用是按值传递的)
  2. 有些数据类型是可变的,但有些不是

所以:

  • 如果将可变对象传递给方法,则该方法将获得对该对象的引用,并且可以对其进行突变,但是如果您将该引用重新绑定到该方法中,则外部作用域对此一无所知完成后,外部参考仍将指向原始对象。

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

为了更加清楚,让我们举一些例子。

列表 - 可变类型

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

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分配一个新列表不会影响方法外部的代码。 the_listouter_list引用的副本,我们让the_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

同样,由于the_string参数是通过值传递的,因此为其分配新的字符串不会影响方法外部的代码。 the_stringouter_string引用的副本,我们有the_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 中的变量有误解。如果您习惯了大多数传统语言,那么您将对以下顺序有一个心理模型:

a = 1
a = 2

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

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

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是单独的并且不会更改。

解决此问题的唯一方法是传递一个可变对象。因为两个引用都引用同一个对象,所以对对象的任何更改都会在两个地方反映出来。

def __init__(self):         
    self.variable = ['Original']
    self.Change(self.variable)

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

我发现其他答案相当冗长和复杂,因此我创建了这个简单的图来解释 Python 处理变量和参数的方式。 在此处输入图片说明

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

http://effbot.org/zone/call-by-object.htm

这是一个重要的报价:

“... 变量 [名称] 不是对象;它们不能由其他变量表示或由对象引用。”

在您的示例中,调用Change方法时 - 为它创建一个名称空间var成为该名称空间中字符串对象'Original' 。然后,该对象在两个名称空间中都有一个名称。接下来, var = 'Changed'var绑定到新的字符串对象,因此该方法的名称空间忘记了'Original' 。最后,忘记了该名称空间,以及字符串'Changed'

考虑一下通过分配而不是通过引用 / 按值传递的内容。这样,就很清楚,只要您了解正常分配过程中发生了什么,就会发生什么情况。

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

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 始终使用按引用传递值 。我将重复其他答案以支持我的发言。

Python 始终使用按引用传递值。也不例外。任何变量分配都意味着复制参考值。没有例外。任何变量都是绑定到参考值的名称。总是。

您可以将参考值视为目标对象的地址。该地址在使用时会自动取消引用。这样,使用参考值,似乎您可以直接使用目标对象。但是,两者之间总是存在一个参考,距离目标还有一步之遥。

这是证明 Python 使用引用传递的示例:

传递参数的图解示例

如果参数通过值传递,则外部lst无法修改。绿色是目标对象(黑色是内部存储的值,红色是对象类型),黄色是内部具有参考值的存储器 - 绘制为箭头。蓝色实心箭头是传递给函数的参考值(通过虚线蓝色箭头路径)。丑陋的深黄色是内部词典。 (实际上也可以将其绘制为绿色椭圆形。颜色和形状仅表示它是内部的。)

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

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

参考值隐藏在 Python 中。没有用于存储参考值的任何明确的用户类型。但是,您可以将 list 元素(或任何其他合适的容器类型的元素)用作参考变量,因为所有容器的确会将这些元素存储为对目标对象的引用。换句话说,元素实际上不包含在容器内,仅包含对元素的引用。

Python 中没有变量

理解参数传递的关键是停止考虑 “变量”。 Python 中有名称和对象,它们一起看起来像变量,但是始终区分这三个名称很有用。

  1. Python 具有名称和对象。
  2. 分配将名称绑定到对象。
  3. 将参数传递给函数还会将名称(函数的参数名称)绑定到对象。

这就是全部。可变性与这个问题无关。

例:

a = 1

这会将名称a绑定到具有值 1 的整数类型的对象。

b = x

这会将名称b绑定到名称x当前绑定到的同一对象。此后,名称b与名称x无关。

请参阅 Python 3 语言参考中的3.14.2节。

如何阅读问题中的例子

在问题所示的代码中,语句self.Change(self.variable)将名称var (在Change函数的范围内self.Change(self.variable)绑定到持有值'Original'和赋值var = 'Changed' (在函数体Change )再次为该对象分配了相同的名称:另一个对象(碰巧也包含一个字符串,但可能完全是其他东西)。

如何通过参考

(编辑 2019-04-28)

因此,如果要更改的对象是可变对象,则没有问题,因为所有内容均通过引用有效地传递。

如果它是一个不可变的对象(例如布尔,数字,字符串),则将其包装在一个可变的对象中。
对此的快速解决方案是一个元素列表(而不是self.variable ,传递[self.variable]并在函数中修改var[0] )。
更加Python 化的方法是引入一个琐碎的,一属性类。该函数接收该类的实例并操纵该属性。

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

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

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

(是的,我知道这可能很不方便,但是有时候这样做很简单。)

(编辑 - 布莱尔更新了他非常受欢迎的答案,现在它是准确的)

我认为重要的是要注意到,当前职位(布莱尔 · 康拉德(Blair Conrad))的票数最多,尽管其结果是正确的,但根据其定义,它具有误导性并且是不正确的。尽管有许多语言(例如 C)允许用户通过引用传递或通过值传递,但 Python 并不是其中一种。

大卫 · 库纳波(David Cournapeau)的答案指出了真正的答案,并解释了为什么布莱尔 · 康拉德(Blair Conrad)的帖子中的行为似乎是正确的,而定义却不正确。

就 Python 按值传递的程度而言,所有语言都是按值传递的,因为必须发送一些数据(“值” 或 “引用”)。但是,这并不意味着 Python 就象 C 程序员会想到的那样按值传递。

如果您想要该行为,Blair Conrad 的答案很好。但是,如果您想了解为什么 Python 既不按值传递也不按引用传递的细节,请阅读 David Cournapeau 的答案。