如何通过引用传递变量?


2627

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

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

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

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


23
有关简短说明/澄清,请参见此stackoverflow问题的第一个答案。由于字符串是不可变的,因此不会更改它们,并且会创建一个新变量,因此“外部”变量仍具有相同的值。
PhilS 2009年

10
BlairConrad的答案中的代码不错,但是DavidCournapeau和DarenThomas提供的解释是正确的。
伊桑·弗曼

70
在阅读所选答案之前,请考虑阅读此简短文本。其他语言具有“变量”,Python具有“名称”。考虑“名称”和“对象”,而不是“变量”和“引用”,您应该避免很多类似的问题。
lqc 2012年

24
为什么这会不必要地使用类?这不是Java:P
Peter R

2
另一个解决方法是创建一个包装“引用”,如下所示:ref = type('',(),{'n':1})stackoverflow.com/a/1123054/409638
robert 2014年

Answers:


2830

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

  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,而不是其副本,因此我们可以使用变异列表方法对其进行更改,并使更改反映在外部作用域中。

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

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

尽管这看起来有点麻烦。


165
然后在C中也一样,当您通过“按引用”传递时实际上是按值传递引用...定义“按引用传递”:P
Andrea Ambu

104
我不确定我是否理解您的条款。我已经退出C游戏有一段时间了,但是回到当初,我就没有“按引用传递”了-您可以传递东西,而且它总是按值传递,因此参数列表中的内容被复制了。但是有时候,事情是一个指针,它可以追随内存(原始,数组,结构等),但是您不能更改从外部范围复制的指针-完成函数后,原始指针仍指向相同的地址。C ++引入了引用,它们的行为有所不同。
布莱尔·康拉德

34
@Zac Bowling在实际意义上,我真的不明白您所说的与这个答案有什么关系。如果Python新手想知道有关通过ref / val进行传递的信息,那么此答案的含义是:1-可以使用函数接收的引用作为其参数,以修改变量的“外部”值,只要因为您没有重新分配参数以引用新对象。2-分配给不可变的类型将始终创建一个新对象,这将破坏您对外部变量的引用。
坎·杰克逊

11
@CamJackson,您需要一个更好的示例-数字在Python中也是不可变的对象。此外,说没有等号左边没有下标的任何赋值是否会将其重新赋值给新对象是不是正确的?def Foo(alist): alist = [1,2,3]不会修改从呼叫者角度列表的内容。
Mark Ransom

59
-1。所示的代码很好,关于如何完全错误的解释。有关原因的正确解释,请参阅DavidCournapeau或DarenThomas的答案。
伊桑·弗曼

685

问题来自对Python中的变量有误解。如果您习惯了大多数传统语言,那么您将对以下顺序有一个心理模型:

a = 1
a = 2

您认为这a是存储值的存储位置1,然后将其更新以存储值2。这不是Python中的工作方式。相反,a首先将其作为对具有值的对象的引用1,然后将其重新分配为对具有value的对象的引用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'

106
很好的解释。我听到的关于“ Python函数参数是通过值传递的引用,这是个相当神秘的短语”的最佳解释之一就是“当您调用函数...时”这一段。我认为,如果您仅了解该段,其他所有内容都将是有意义的,并且可以从此处作为逻辑结论得出。然后,您只需要知道何时创建新对象以及何时修改现有对象即可。
·杰克逊

3
但是如何重新分配参考?我以为您不能更改'var'的地址,但是您的字符串“ Changed”现在将存储在'var'的内存地址中。您的描述使“ Changed”和“ Original”看起来似乎属于内存中的不同位置,而您只是将'var'切换到了不同的地址。那是对的吗?
Glassjawed 2012年

8
@Glassjawed,我想你明白了。“ Changed”和“ Original”是位于不同内存地址的两个不同的字符串对象,“ var”从指向一个指向另一个指向的更改。
马克·兰瑟姆

使用id()函数有助于澄清问题,因为它可以清楚地说明Python创建新对象的时间(无论如何,我认为)。
蒂姆·理查森

1
用最简单的术语@MinhTran来说,引用是“引用”对象的东西。该物理表示很可能是一个指针,但这只是实现细节。本质上,这确实是一个抽象概念。
马克·兰瑟姆

329

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


2
可爱,可以轻松发现存在中间任务的细微差别,这对随便的旁观者而言并不明显。+1
user22866 '16

9
A是否可变并不重要。如果您为B分配了其他内容,则A不会改变。如果某个对象是可变的,那么可以将其突变。但这与直接分配名称
彼得斯

1
@Martijn你是对的。我删除了答案中提到可变性的部分。我认为现在没有比这更简单的了。
Zenadix

4
感谢您的更新,更好!使大多数人感到困惑的是订阅的分配;例如B[0] = 2,与直接分配B = 2
马丁·彼得斯

11
“ A被分配给B。” 这不是模棱两可的吗?我认为用普通英语可以表示A=BB=A
Hatshepsut

240

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

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

这是一个重要的报价:

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

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


21
我觉得很难买到。对我来说,就像Java一样,参数是指向内存中对象的指针,并且这些指针是通过堆栈或寄存器传递的。
卢西亚诺

10
这不像Java。不相同的情况之一是不可变对象。考虑一下琐碎的函数lambda x:x。将其应用于x = [1,2,3]和x =(1,2,3)。在第一种情况下,返回值将是输入的副本,在第二种情况下返回相同。
大卫·库纳波

26
不,这正是像Java的语义对象。我不确定“在第一种情况下,返回值将是输入的副本,而在第二种情况下相同”的含义。但该说法显然是错误的。
Mike Graham

23
它与Java完全相同。对象引用按值传递。任何有不同想法的人都应该为swap可以交换两个引用的函数附加Python代码,例如: a = [42] ; b = 'Hello'; swap(a, b) # Now a is 'Hello', b is [42]
cayhorstmann 2012年

24
在Java中传递对象时,它与Java完全相同。但是,Java也具有原语,通过复制原语的值来传递它们。因此,它们在这种情况下有所不同。
Claudiu

174

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

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

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

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


3
乍一看,这个答案似乎避开了原来的问题。经过一番阅读,我开始意识到这很清楚。在这里可以找到有关“名称分配”概念的良好跟进方法:像Pythonista一样的代码:惯用的Python
Christian Groleau

65

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

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

  • 当您进行诸如的分配时x = 1000,将创建一个字典条目,该条目将当前名称空间中的字符串“ x”映射到指向包含一千的整数对象的指针。

  • 当您使用来更新“ x”时x = 2000,将创建一个新的整数对象,并且字典将更新为指向该新对象。旧的一千个对象保持不变(取决于其他是否引用了该对象,该对象是否可以存活)。

  • 当您进行诸如的新分配时y = x,将创建一个新的字典条目“ y”,该条目指向与“ x”条目相同的对象。

  • 诸如字符串和整数之类的对象是不可变的。这仅表示没有任何方法可以在创建对象后更改该对象。例如,一旦创建了整数对象1000,它就永远不会改变。数学是通过创建新的整数对象完成的。

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

希望能为您解决问题。


2
我非常感谢从开发人员那里了解到这一点。id()正如pepr的答案所暗示的那样,该函数是否返回指针(对象引用)的值?
诚实安倍晋三

4
@HonestAbe是的,在CPython中,id()返回地址。但是在其他python(例如PyPy和Jython)中,id()只是唯一的对象标识符。
Raymond Hettinger 2014年

59

从技术上讲,Python始终使用按引用传递值。我将重复其他答案以支持我的发言。

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

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

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

传递参数的图解示例

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

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

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

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


1
实际上这是通过参考值确认的。尽管示例不是很好,但此答案+1。
BugShotGG 2012年

30
发明新的术语(例如“按参考值传递”或“按对象调用”无用)。“按(值|引用|名称)调用”是标准术语。“参考”是一个标准术语。使用标准术语,按值传递引用可以准确地描述Python,Java和许多其他语言的行为。
cayhorstmann

4
@cayhorstmann:问题在于Python变量的术语含义与其他语言不同。这样,按引用调用在这里就不太适合。另外,您如何精确定义术语“ 参考”?非正式地,Python方式可以很容易地描述为传递对象的地址。但这与潜在的Python实现不兼容。
pepr

1
我喜欢这个答案,但是您可能会考虑该示例是否确实在帮助或损害了流程。另外,如果将“参考值”替换为“对象参考”,则将使用我们认为是“官方”的术语,如下所示:定义函数
诚实的安倍晋三2014年

2
该引号的末尾有一个脚注,内容为:“实际上,按对象引用进行调用将是更好的描述,因为如果传递了可变对象,则调用者将看到被调用者对其进行的任何更改... “我同意你的看法,这是由于试图适应其他语言建立的术语而引起的。除了语义,需要理解的是:词典/名称空间,名称绑定操作以及名称→指针→对象的关系(如您所知)。
诚实的安倍晋三2014年

56

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(在function的范围内Change)绑定到保存该值的对象,'Original'而赋值var = 'Changed'(在function的主体中Change)再次将同一个名称分配给其他对象(发生这种情况)也可以容纳一个字符串,但完全可以是其他东西)。

如何通过参考

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

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


20
“ Python没有变量”是一个愚蠢而令人困惑的口号,我真的希望人们不要再说它了……:(此答案的其余部分很好!
Ned Batchelder 2014年

9
这可能令人震惊,但并不傻。而且我也不认为这令人困惑:它希望打开接收者对即将到来的解释的思想,并使她处于一种有用的“我想知道他们有什么而不​​是变量”的态度。(是的,您的里程可能会有所不同。)
Lutz Prechelt 2014年

13
您还会说Javascript没有变量吗?它们的工作方式与Python相同。另外,Java,Ruby,PHP等。我认为一种更好的教学技术是“ Python的变量与C的变量工作方式不同”。
Ned Batchelder 2014年

9
是的,Java有变量。Python,JavaScript,Ruby,PHP等也是如此。在Java中您不会说int声明了变量,但Integer没有声明。它们都声明变量。该Integer变量是一个对象,该int变量是一个原语。举例来说,您通过显示演示了变量的工作方式a = 1; b = a; a++ # doesn't modify b。在Python中也是如此(+= 1因为++Python中没有,所以使用了)!
Ned Batchelder 2014年

1
“变量”的概念很复杂,而且常常含糊不清:变量是值的容器,由名称标识。在Python中,值是对象,容器是对象(请参阅问题?),名称实际上是分开的东西。我相信以这种方式准确地了解变量要困难得多。名称和对象的解释似乎比较困难,但实际上更简单。
Lutz Prechelt 2015年

43

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

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

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

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


7
+1表示少量文本,为解决Python没有传递引用的问题提供了必要的解决方法。(作为适用于此页面以及此页面上任何地方的后续注释/问题:我尚不清楚为什么python无法像C#那样提供“ ref”关键字,它只是将调用者的参数包装在一个列表中,例如这,并将对函数内参数的引用视为列表的第0个元素。)
M Katz 2014年

5
真好 要通过引用传递,请使用[]换行。
Justas 2015年

38

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

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

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

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

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


4
并非所有语言都按值调用是不正确的。在C ++或Pascal(当然还有许多我不知道的其他语言)中,您可以通过引用进行调用。例如,在C ++中,void swap(int& x, int& y) { int temp = x; x = y; y = temp; }将交换传递给它的变量。在Pascal中,您使用var代替&
cayhorstmann

2
我以为我早就回答了这个问题,但我看不到。为了完整起见,cayhorstmann误解了我的回答。我并不是说,大多数人最先了解的有关C / C ++的术语都是按价值要求的。只是简单地传递了一些值(值,名称,指针等),并且布莱尔的原始答案中使用的术语不准确。
神户约翰·约翰逊2015年

24

您在这里得到了一些非常好的答案。

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

2
是的,但是如果您执行x = [2,4,4,5,5],y = x,X [0] = 1,则打印x#[1,4,4,5,5]打印y#[1 、、
4、4、5、5

19

在这种情况下,var方法中标题为该变量Change的引用将分配给self.variable,然后您立即将一个字符串分配给var。它不再指向self.variable。以下代码段显示了如果修改由var和指向的数据结构self.variable(在本例中为列表)将会发生的情况:

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

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


18

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

  • 不变的参数有效地“ 按值 ” 传递。诸如整数和字符串之类的对象是通过对象引用传递的,而不是通过复制传递的,但是由于您无论如何都不能就地更改不可变对象,因此效果很像制作副本。
  • 可变参数有效地“ 通过指针 ” 传递。诸如列表和字典之类的对象也通过对象引用传递,这类似于C将数组作为指针传递的方式—可变对象可以在函数中就地更改,就像C数组一样。

16

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

http://docs.python.org/3/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

例:

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

5
我很想发表类似的回答-最初的提问者可能不知道他想要的实际上是使用在函数之间共享的全局变量。这是我本应共享的链接:stackoverflow.com/questions/423379/… 作为对@Tim的回答,Stack Overflow不仅是一个问答网站,而且它是一个庞大的参考知识资料库,只会变得更强大,更细微得多。就像一个活跃的Wiki,需要更多的输入。
Max P Magee 2014年

13

答案中有很多见解,但我认为此处没有明确提及其他要点。引用python文档https://docs.python.org/2/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

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

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

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

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


10

这是pass by objectPython中使用的概念的简单解释(我希望如此)。
每当将对象传递给函数时,都会传递对象本身(Python中的对象实际上是您在其他编程语言中称为值的对象),而不是对该对象的引用。换句话说,当您致电时:

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

my_list = [0, 1]
change_me(my_list)

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

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

这显然不会改变传递给函数的对象。如果函数看起来像这样:

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

然后,该调用将导致:

[0, 1].append(2)

这显然会改变对象。这个答案很好地解释了。


2
问题是作业没有执行您期望的其他事情。的list = [1, 2, 3]原因重用list其他东西的名称和忘却最初传递的对象。但是,您可以尝试list[:] = [1, 2, 3](顺便说一句list,变量的名称错误。想想这[0, 1] = [1, 2, 3]是一个完全的废话。无论如何,您认为对象本身已传递了什么意思?您认为复制到该函数的内容是什么?
pepr

@pepr对象不是文字。它们是对象。谈论他们的唯一方法是给他们起一些名字。这就是为什么一旦掌握它就这么简单,但是解释起来却非常复杂。:-)
Veky

@Veky:我知道。无论如何,列表文字会转换为列表对象。实际上,Python中的任何对象都可以不带名称而存在,并且即使没有给出任何名称也可以使用它。您可以将它们视为匿名对象。考虑对象是列表的元素。他们不需要名字。您可以通过索引或遍历列表来访问它们。无论如何,我坚持[0, 1] = [1, 2, 3]只是一个不好的例子。Python中没有类似的东西。
pepr 2014年

@pepr:我不一定要指Python定义名称,而只是普通名称。当然可以alist[2]算作alist的第三个元素的名称。但是我想我误会了你的问题。:-)
Veky

啊 我的英语显然比Python差很多。:-)我会再尝试一次。我只是说过,您必须给对象起一些名字来谈论它们。我所说的“名称”不是指“ Python定义的名称”。我知道Python机制,不用担心。
Veky

8

除了对这些东西如何在Python中工作的所有出色解释之外,我没有看到关于该问题的简单建议。正如您似乎确实要创建对象和实例一样,处理实例变量并更改它们的pythonic方法如下:

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

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

在实例方法中,通常引用self访问实例属性。通常__init__在实例方法中设置实例属性并对其进行读取或更改。这也是为什么您将selfals的第一个参数传递给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

6

即使该语言无法实现通过引用传递对象的小技巧,它也适用于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


p是对可变列表对象的引用,该对象又存储了该对象obj。引用'p'被传递到中changeRef。在内部changeRef,将创建一个新引用(称为ref),该引用指向指向该列表的对象p。但是因为列表是可变的,所以两个引用都可以看到列表的更改。在这种情况下,您使用ref引用来更改索引为0的对象,以便随后存储该PassByReference('Michael')对象。列表对象的ref更改已使用完成,但此更改对可见p
Minh Tran

因此,现在,引用pref指向存储单个对象的列表对象PassByReference('Michael')。因此,随之p[0].name返回Michael。当然,ref现在已经超出范围,可能会被垃圾回收,但是都一样。
Minh Tran

不过,您尚未更改与引用关联name的原始PassByReference对象的私有实例变量obj。其实obj.name会回来的Peter。前面的评论假定给出了定义Mark Ransom
Minh Tran

重要的是,我不同意这是一种黑客行为(我指的是指某些有效的工具,但出于实施者未知,未经测试或意外的原因)。您只需将一个PassByReference对象替换为PassByReference列表中的另一个对象,然后引用两个对象中的后者即可。
Minh Tran

5

我使用以下方法快速将几个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

是的,这也解决了我的用例中的“按引用传递”。我有一个基本上清除a中的值dict然后返回的函数dict。但是,在清理过程中很明显,需要重建系统的一部分。因此,该功能不仅必须返回已清除的内容,dict而且还必须能够发出重建信号。我试图bool通过引用传递,但是ofc无效。在弄清楚如何解决这个问题后,我发现您的解决方案(基本上是返回一个元组)最有效,同时也完全不是黑客/变通方法(IMHO)。
卡西米尔

3

虽然按引用传递并不是最适合python的东西,并且不应该使用,但是有些变通办法实际上可以起作用,以获取当前分配给局部变量的对象,或者甚至从调用函数内部重新分配局部变量。

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

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

def change(wrapper):
    wrapper(7)

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

相同的想法适用于读取和del设置变量。

对于正读,甚至有一种更短的使用方法,lambda: x该方法返回可调用对象,而被调用时将返回x的当前值。这有点像遥远的过去在语言中使用的“按名称呼叫”。

传递3个包装器来访问变量有点麻烦,因此可以将它们包装到具有proxy属性的类中:

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

借助Python的“反射”支持,可以获得一个对象,该对象能够在给定范围内重新分配名称/变量,而无需在该范围内显式定义函数:

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文档建议更改字典可能不起作用,但似乎对我有用。


3

给定python处理值和对其的引用的方式,您可以引用任意实例属性的唯一方法是按名称进行:

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

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

当然,在实际代码中,您将在dict查找上添加错误检查。


3

由于字典是通过引用传递的,因此可以使用dict变量在其中存储任何引用的值。

# returns the result of adding numbers `a` and `b`
def AddNumbers(a, b, ref): # using a dict for reference
    result = a + b
    ref['multi'] = a * b # reference the multi. ref['multi'] is number
    ref['msg'] = "The result: " + str(result) + " was nice!" # reference any string (errors, e.t.c). ref['msg'] is string
    return result # return the sum

number1 = 5
number2 = 10
ref = {} # init a dict like that so it can save all the referenced values. this is because all dictionaries are passed by reference, while strings and numbers do not.

sum = AddNumbers(number1, number2, ref)
print("sum: ", sum)             # the return value
print("multi: ", ref['multi'])  # a referenced value
print("msg: ", ref['msg'])      # a referenced value

3

Python中的按引用传递与C ++ / Java中的按引用传递概念完全不同。

  • Java&C#:原始类型(包括字符串)通过值(副本)传递,引用类型通过引用(地址副本)传递,因此对调用者可见的所有被调用函数中的参数更改。
  • C ++:允许按引用传递或按值传递。如果参数是通过引用传递的,则可以根据是否以const形式传递参数来对其进行修改。但是,无论是否为const,该参数都将保留对对象的引用,并且无法将引用分配为指向所调用函数内的另一个对象。
  • Python: Python是“按对象传递引用”,通常这样说:“对象引用按值传递。” [阅读此处] 1。调用者和函数都引用相同的对象,但是函数中的参数是一个新变量,它仅在调用者中保存对象的副本。像C ++一样,可以在函数中修改或不修改参数-这取决于传递的对象的类型。例如; 不变的对象类型不能在调用的函数中修改,而可变的对象可以更新或重新初始化。更新或重新分配/重新初始化可变变量之间的关键区别在于,更新后的值会反映在被调用函数中,而重新初始化后的值则不会。将新对象分配给可变变量的范围是python中函数的局部范围。@ blair-conrad提供的示例很好地理解了这一点。

1
很老,但我不得不纠正它。字符串通过引用在Java和C#传递,而不是价值
约翰

2

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

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
虽然这有效。它不通过引用传递。这是“按对象引用传递”。
比什瓦斯·米什拉

1

您只能将一个空类用作存储参考对象的实例,因为内部对象属性存储在实例字典中。参见示例。

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)

1

由于似乎没有地方提到,例如C ++等已知的模拟引用的方法是使用“更新”函数并传递该函数而不是实际变量(或更确切地说,是“名称”):

def need_to_modify(update):
    update(42) # set new value 42
    # other code

def call_it():
    value = 21
    def update_value(new_value):
        nonlocal value
        value = new_value
    need_to_modify(update_value)
    print(value) # prints 42

这对于“仅引用”或在具有多个线程/进程的情况下(通过使更新功能线程/多重处理安全)最有用。

显然,以上内容不允许读取值,而只能对其进行更新。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.