题 如何通过引用传递变量?


Python文档似乎不清楚参数是通过引用还是值传递,以下代码生成未更改的值'Original'

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

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

有什么我可以通过实际参考传递变量吗?


2055
2018-06-12 10:23


起源


有关简短说明/澄清,请参阅第一个答案 这个stackoverflow问题。由于字符串是不可变的,因此不会更改它们并创建新变量,因此“外部”变量仍具有相同的值。 - PhilS
BlairConrad的答案中的代码很好,但DavidCournapeau和DarenThomas提供的解释是正确的。 - Ethan Furman
在阅读所选答案之前,请考虑阅读这篇简短的文字 其他语言有“变量”,Python有“名字”。想想“名字”和“对象”而不是“变量”和“引用”,你应该避免很多类似的问题。 - lqc
为什么这不必要地使用一个类?这不是Java:P - Peter R
另一种解决方法是创建一个包装器'reference',如下所示:ref = type('',(),{'n':1}) stackoverflow.com/a/1123054/409638 - robert


答案:


争论是 通过任务传递。这背后的理由是双重的:

  1. 传入的参数实际上是a 参考 到一个对象(但引用按值传递)
  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,而不是它的副本,我们可以使用变异列表方法来更改它,并将更改反映在外部范围中。

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

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 参考,我们有 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_string 是一份副本 outer_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])

虽然这看起来有点麻烦。


2225
2018-06-12 11:18



然后同样在C中,当你通过“引用”时,你实际上正在通过 按价值 引用...定义“按引用”:P - Andrea Ambu
我不确定我理解你的条款。我已经离开了C游戏一段时间了,但当我进入它时,没有“通过参考传递” - 你可以传递东西,它总是按值传递,所以参数列表中的任何内容被复制了。但有时候东西是一个指针,它可以跟随内存(原始,数组,结构,等等),但你无法改变从外部作用域复制的指针 - 当你完成了这个功能,原始指针仍指向同一地址。 C ++引入了一些表现不同的引用。 - Blair Conrad
@Zac保龄球我真的不明白你所说的话在实际意义上与这个答案有关。如果一个Python新手想知道通过ref / val传递,那么这个答案的结果是: 1- 您 能够 使用函数接收的引用作为其参数,修改变量的“外部”值,只要不重新分配参数以引用新对象即可。 2- 分配给不可变类型 总是 创建一个新对象,它打破了你对外部变量的引用。 - Cam Jackson
@CamJackson,你需要一个更好的例子 - 数字也是Python中的不可变对象。此外,说这不是真的 任何 在equals左侧没有下标的赋值会将名称重新分配给新对象,无论它是否是不可变的? def Foo(alist): alist = [1,2,3] 将 不 从调用者的角度修改列表的内容。 - Mark Ransom
-1。显示的代码很好,解释如何完全错误。请参阅DavidCournapeau或DarenThomas的答案,以获得有关原因的正确解释。 - Ethan Furman


问题来自对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'

530
2017-11-15 17:45



简洁明了的解释。你的段落“当你调用一个函数......”是我听说过的最好的解释之一,即'Python函数参数是引用,通过值传递的相当神秘的短语'。我想如果你单独理解那个段落,那么其他一切只是有意义并从那里作为一个合乎逻辑的结论流动。然后,您只需要知道何时创建新对象以及何时修改现有对象。 - Cam Jackson
但是,如何重新分配参考?我以为你不能改变'var'的地址,但你的字符串“Changed”现在将被存储在'var'内存地址中。您的描述使其看起来像“已更改”和“原始”属于内存中的不同位置而您只需将“var”切换到其他地址即可。那是对的吗? - Glassjawed
@Glassjawed,我想你已经明白了。 “Changed”和“Original”是不同内存地址处的两个不同的字符串对象,“var”从指向一个指向另一个指向另一个。 - Mark Ransom
这是最好的解释,应该是公认的答案。简洁明了。其他人投入了新的,令人困惑的术语,如“参考通过价值传递”,“称为对象”等。这个答案清除了一切。谢谢 :) - ajay
使用id()函数有助于澄清问题,因为它在Python创建新对象时很清楚(所以我想,无论如何)。 - Tim Richardson


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


243
2017-09-04 16:05



你的图表很好地阐明了潜在的概念,虽然我不会打电话给其他答案 漫长而复杂。 - Bruce Wayne
一个非常优雅的解释。我认为其他答案也很漫长而复杂。做得好! - kirk.burleson
A是否可变是无关紧要的。如果你给B分配不同的东西, A不会改变。如果一个对象是可变的,你肯定可以改变它。但这与直接分配名称无关。 - Martijn Pieters♦
感谢您的更新,更好!让大多数人感到困惑的是分配订阅;例如 B[0] = 2,与直接任务, B = 2。 - Martijn Pieters♦
“A被分配给B.”那不是暧昧吗?我认为普通英语也可能意味着 A=B 要么 B=A。 - Hatshepsut


它既不是按值传递,也不是按引用传递 - 它是逐个调用的。请见Fredrik Lundh:

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

这是一个重要的引用:

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

在你的例子中,当 Change 方法被称为 - a 命名空间 是为它而创造的;和 var 成为字符串对象中该名称空间内的名称 'Original'。然后,该对象在两个名称空间中具有名称。下一个, var = 'Changed' 结合 var 到一个新的字符串对象,因此该方法的命名空间忘记了 'Original'。最后,忘记了该命名空间,以及字符串 'Changed' 随之而来。


220
2018-06-12 12:55



我觉得很难买。对我而言就像Java一样,参数是指向内存中对象的指针,这些指针通过堆栈或寄存器传递。 - Luciano
这不像java。不一样的情况之一是不可变对象。想想lambda x:x的简单函数。将此应用于x = [1,2,3]和x =(1,2,3)。在第一种情况下,返回的值将是输入的副本,并且在第二种情况下是相同的。 - David Cournapeau
不,它是 究竟 就像Java对象的语义一样。我不确定你的意思是“在第一种情况下,返回的值将是输入的副本,在第二种情况下是相同的。”但这种说法似乎显然是不正确的。 - Mike Graham
它与Java完全相同。对象引用按值传递。任何以不同方式思考的人都应该附加一个Python代码 swap 可以交换两个引用的函数,如下所示: a = [42] ; b = 'Hello'; swap(a, b) # Now a is 'Hello', b is [42] - cayhorstmann
在Java中传递对象时,它与Java完全相同。但是,Java也有原语,它们通过复制原语的值来传递。因此,他们在这种情况下不同。 - Claudiu


想想被传递的东西 通过转让 而不是通过引用/按值。这样,只要您了解正常分配期间发生的情况,就可以清楚地知道发生了什么。

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

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

由于不可变类型无法修改,因此 似乎 就像通过值传递一样 - 将int传递给函数意味着将int赋值给functions参数。您只能重新分配,但不会更改原始变量值。


142
2018-06-12 12:17



乍一看,这个答案似乎回避了原来的问题。经过第二次阅读后,我逐渐意识到这使事情变得非常明确。可以在此处找到对此“名称分配”概念的良好跟进: 代码像Pythonista:惯用Python - Christian Groleau


Effbot(又名Fredrik Lundh)将Python的变量传递样式描述为call-by-object: http://effbot.org/zone/call-by-object.htm

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

  • 当你做出如 x = 1000,创建一个字典条目,将当前名称空间中的字符串“x”映射到指向包含一千个整数对象的指针。

  • 用“...”更新“x”时 x = 2000,创建一个新的整数对象,并更新字典以指向新对象。旧的一千个对象不变(并且可能存在也可能不存在,具体取决于是否有任何其他对象引用)。

  • 当你做一个新的任务,如 y = x,创建一个新的词典条目“y”,它指向与“x”的条目相同的对象。

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

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

希望能为您澄清问题。


53
2018-03-29 04:41



我真的很感激从开发人员那里学到这一点。这是真的吗? id() 函数返回指针的(对象引用)值,如pepr的答案所示? - Honest Abe
@HonestAbe是的,在CPython中 ID() 返回地址。但在其他蟒蛇如PyPy和Jython中 ID() 只是一个唯一的对象标识符。 - Raymond Hettinger
其他答案让我感到茫然和困惑。这个清除了一切。谢谢。 - shongololo
这个答案,以及使用id()的提示,它显示赋值将现有名称绑定到新对象时,在整个线程中是最好的。 - Tim Richardson
这个答案是在刑事上被低估了。 - wandadars


从技术上讲, Python总是使用传递引用值。我要重复一遍 我的另一个答案 支持我的发言。

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

您可以将参考值视为目标对象的地址。使用时会自动取消引用该地址。这样,使用参考值,您似乎直接使用目标对象。但总会有一个参考,更多的是跳到目标。

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

Illustrated example of passing the argument

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

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

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

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


53
2017-09-15 18:53



实际上,这是通过参考值确认的。尽管这个例子并不好,但这个答案还是+1。 - BugShotGG
发明新术语(例如“按引用值传递”或“按对象调用”没有帮助)。 “呼叫(值|参考|名称)”是标准术语。 “参考”是一个标准术语。按值传递引用使用标准术语准确地描述了Python,Java和许多其他语言的行为。 - cayhorstmann
@cayhorstmann:问题在于 Python变量 与其他语言的术语含义不同。这条路, 通过引用打电话 在这里不合适。另外,你好吗? 究竟 定义术语 参考?非正式地,Python方式可以很容易地描述为传递对象的地址。但它不适合Python的潜在分布式实现。 - pepr
我喜欢这个答案,但你可能会考虑这个例子是否真的有助于或伤害流程。此外,如果您将“参考值”替换为“对象引用”,您将使用我们可以认为是“官方”的术语,如下所示: 定义功能 - Honest Abe
该引言末尾有一个脚注,内容如下: “其实, 按对象引用调用 将是一个更好的描述,因为如果传递一个可变对象,调用者将看到被调用者对它做出的任何更改......“ 我同意你的看法,认为混淆是因为试图使用其他语言建立的术语。除了语义之外,需要理解的是:字典/命名空间, 名称绑定操作 以及名称→指针→对象的关系(如您所知)。 - Honest Abe


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

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

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

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


36
2017-08-05 22:52



为少量文本+1,为Python没有传递引用的问题提供必要的解决方法。 (作为一个适合此处以及本页任何地方的后续评论/问题:我不清楚为什么python不能像C#那样提供“ref”关键字,它只是将调用者的参数包含在列表中这个,并将函数中对参数的引用视为列表的第0个元素。) - M Katz
尼斯。要通过ref传递,请换入[]的。 - Justas
伟大的,为避免所有的教诲做得很好。 - Puffin


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

我认为值得注意的是,目前投票率最高的帖子(布莱尔康拉德)虽然在结果方面是正确的,但却是误导性的,并且基于其定义是不正确的。虽然有许多语言(如C)允许用户通过引用传递或按值传递,但Python不是其中之一。

David Cournapeau的回答指出了真正的答案,并解释了为什么布莱尔康拉德的帖子中的行为似乎是正确的,而定义则不然。

在Python按值传递的范围内,所有语言都是按值传递的,因为必须发送一些数据(无论是“值”还是“引用”)。但是,这并不意味着Python在C程序员会想到它的意义上是通过值传递的。

如果你想要这种行为,布莱尔康拉德的回答很好。但是如果你想知道为什么Python既没有通过值传递也没有通过引用传递,那么请阅读David Cournapeau的回答。


34
2018-06-27 12:33



是的,我认为David Cournapeau给出的链接是更完整的描述 - David Sykes
根本不是所有语言都是按值调用的。在C ++或Pascal(当然还有许多其他我不知道的内容)中,您可以通过引用进行调用。例如,在C ++中, void swap(int& x, int& y) { int temp = x; x = y; y = temp; } 将交换传递给它的变量。在Pascal中,您使用 var 代替 &。 - cayhorstmann
我以为我很久以前就回复了这个,但我没有看到它。为了完整 - cayhorstmann误解了我的答案。我并不是说一切都是按价值呼唤的 在大多数人第一次学习C / C ++的术语中。就是这样 一些 传递值(值,名称,指针等),并且Blair原始答案中使用的术语不准确。 - KobeJohn