ChatGPT解决这个技术问题 Extra ChatGPT

如何通过引用传递变量?

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

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

    def change(self, var):
        var = 'Changed'
有关简短说明/说明,请参阅 this stackoverflow question 的第一个答案。由于字符串是不可变的,它们不会被更改并且会创建一个新变量,因此“外部”变量仍然具有相同的值。
BlairConrad 答案中的代码很好,但 DavidCournapeau 和 DarenThomas 提供的解释是正确的。
在阅读所选答案之前,请考虑阅读此简短文本 Other languages have "variables", Python has "names"。考虑“名称”和“对象”而不是“变量”和“引用”,你应该避免很多类似的问题。
另一种解决方法是像这样创建一个包装器“引用”: ref = type('', (), {'n':1}) stackoverflow.com/a/1123054/409638

r
random_user

参数是 passed by assignment。这背后的理由是双重的:

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

所以:

如果你将一个可变对象传递给一个方法,该方法会获得对同一个对象的引用,你可以随心所欲地改变它,但是如果你在方法中重新绑定引用,外部作用域将一无所知,并且之后大功告成,外部引用仍将指向原始对象。

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

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

List - 可变类型

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

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_listouter_list 引用的副本,我们让 the_list 指向一个新列表,但无法更改 outer_list 指向的位置。

String - 不可变类型

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

现在,让我们尝试更改参考

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

虽然这看起来有点麻烦。


然后在 C 中也是如此,当您通过“按引用”传递时,您实际上是按值传递引用...定义“按引用”:P
我不确定我是否理解你的条款。我已经退出 C 游戏有一段时间了,但是当我参与其中时,没有“按引用传递”——你可以传递东西,它总是按值传递,所以参数列表中的任何内容被复制了。但有时事情是一个指针,它可以跟随到一块内存(原始,数组,结构,等等),但是你不能改变从外部范围复制的指针 - 当你完成函数时,原来的指针仍然指向同一个地址。 C++ 引入了引用,其行为不同。
@Zac Bowling我真的不明白你所说的在实际意义上与这个答案有什么关系。如果 Python 新手想知道通过 ref/val 传递,那么从这个答案中得出的结论是: 1- 您可以使用函数接收的引用作为其参数,来修改变量的“外部”值,只要因为您没有重新分配参数来引用新对象。 2-分配给不可变类型将始终创建一个新对象,这会破坏您对外部变量的引用。
@CamJackson,你需要一个更好的例子——数字在 Python 中也是不可变的对象。此外,如果在等号左侧没有下标的 any 分配将重新分配名称到一个新对象,无论它是否不可变,这不是真的吗? def Foo(alist): alist = [1,2,3]不会从调用者的角度修改列表的内容。
-1。显示的代码很好,关于如何的解释是完全错误的。有关原因的正确解释,请参阅 DavidCournapeau 或 DarenThomas 的答案。
r
random_user

问题来自对 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 函数参数是引用,按值传递”这个相当神秘的短语的最佳解释之一。我认为,如果您仅理解该段落,那么其他所有内容都是有意义的,并且从那里得出合乎逻辑的结论。然后,您只需要注意何时创建新对象以及何时修改现有对象。
但是如何重新分配参考?我以为您不能更改“var”的地址,但您的字符串“已更改”现在将存储在“var”内存地址中。您的描述使“已更改”和“原始”似乎属于内存中的不同位置,而您只需将“var”切换到不同的地址。那是对的吗?
@Glassjawed,我想你明白了。 “Changed”和“Original”是不同内存地址的两个不同字符串对象,“var”从指向一个变为指向另一个。
@TonySuffolk66 id 给出了引用对象的标识,而不是引用本身。
@MinhTran 用最简单的术语来说,引用是“引用”对象的东西。它的物理表示很可能是一个指针,但这只是一个实现细节。这实际上是一个抽象的概念。
Z
Zenadix

https://i.stack.imgur.com/FdaCu.png


可爱的,可以很容易地发现中间任务的细微差异,对于不经意的旁观者来说并不明显。 +1
A 是否可变并不重要。如果你给 B 分配不同的东西,A 不会改变。如果一个对象是可变的,你可以改变它,当然。但这与直接分配给名称无关。
@Martijn你是对的。我删除了提到可变性的答案部分。我不认为它现在可以变得更简单了。
感谢更新,好多了!让大多数人感到困惑的是订阅的分配。例如B[0] = 2,与直接分配,B = 2
“A 分配给 B。”这不是模棱两可吗?我认为在普通英语中可以表示 A=BB=A
w
waxwing

它既不是按值传递也不是按引用传递 - 它是按对象调用。看这个,Fredrik Lundh:

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

这是一个重要的报价:

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

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


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

想想通过赋值而不是通过引用/按值传递的东西。这样一来,只要您了解正常分配期间发生的事情,就会很清楚发生了什么。

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

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

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


乍一看,这个答案似乎回避了最初的问题。在第二次阅读之后,我意识到这让事情变得非常清楚。可以在此处找到对“名称分配”概念的良好跟进:Code Like a Pythonista: Idiomatic Python
f
faressalem

Python中没有变量

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

Python 有名称和对象。赋值将名称绑定到对象。将参数传递给函数还会将名称(函数的参数名称)绑定到对象。

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

例子:

a = 1

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

b = x

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

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

如何阅读问题中的示例

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

如何通过引用传递

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

如果它是一个 immutable 对象(例如,布尔值、数字、字符串),则可以将其包装在一个可变对象中。
快速而简单的解决方案是单元素列表 (代替 self.variable,传递 [self.variable] 并在函数中修改 var[0])。
更多的 pythonic 方法是引入一个微不足道的单属性类。该函数接收该类的一个实例并操作该属性。


“Python 没有变量”是一个愚蠢而令人困惑的口号,我真的希望人们不要再这么说了...... :( 这个答案的其余部分很好!
这可能令人震惊,但并不愚蠢。而且我认为这也不令人困惑:它希望能够为即将到来的解释打开接收者的思想,并使她处于一种有用的“我想知道他们有什么而不是变量”的态度。 (是的,您的里程可能会有所不同。)
你还会说 Javascript 没有变量吗?它们的工作方式与 Python 相同。此外,Java、Ruby、PHP ……我认为更好的教学技术是“Python 的变量工作方式与 C 的不同”。
是的,Java 有变量。 Python 和 JavaScript、Ruby、PHP 等也是如此。您不会在 Java 中说 int 声明了一个变量,但 Integer 没有。他们都声明了变量。 Integer 变量是一个对象,int 变量是一个原语。例如,您通过显示 a = 1; b = a; a++ # doesn't modify b 演示了变量的工作方式。在 Python 中也是如此(使用 += 1,因为 Python 中没有 ++)!
“变量”的概念很复杂,而且通常很模糊:变量是值的容器,由名称标识。在 Python 中,值是对象,容器是对象(看到问题了吗?),名称实际上是独立的事物。我相信以这种方式准确理解变量要困难得多。名称和对象的解释看起来更难,但实际上更简单。
R
Raymond Hettinger

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

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

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

当您使用 x = 2000 更新“x”时,将创建一个新的整数对象,并且更新字典以指向新对象。旧的 1000 个对象没有改变(并且可能存在也可能不存在,具体取决于是否有其他对象引用该对象)。

当您执行新的赋值(例如 y = x)时,会创建一个新的字典条目“y”,它指向与“x”条目相同的对象。

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

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

希望能为您澄清问题。


我非常感谢从开发人员那里了解这一点。正如 pepr 的回答所暗示的那样,id() 函数是否返回指针的(对象引用的)值?
@HonestAbe 是的,在 CPython 中 id() 返回地址。但在 PyPy 和 Jython 等其他 python 中,id() 只是一个唯一的对象标识符。
C
Community

从技术上讲,Python 总是使用传递引用值。我将重复 my other answer 以支持我的陈述。

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

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

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

https://i.stack.imgur.com/uzXcP.png

如果参数是按值传递的,则无法修改外部 lst。绿色是目标对象(黑色是里面存储的值,红色是对象类型),黄色是里面有引用值的内存——如箭头所示。蓝色实心箭头是传递给函数的参考值(通过蓝色虚线箭头路径)。丑陋的深黄色是内部字典。 (它实际上也可以画成一个绿色的椭圆。颜色和形状只是说它是内部的。)

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

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

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


实际上这是通过参考值证实了它的通过。尽管这个例子不好,但这个答案+1。
发明新的术语(例如“按引用值传递”或“按对象调用”没有帮助)。 “由(值|参考|名称)调用”是标准术语。 “参考”是一个标准术语。按值传递引用准确地描述了 Python、Java 和许多其他语言的行为,使用标准术语。
@cayhorstmann:问题在于 Python 变量的术语含义与其他语言不同。这样,引用调用在这里就不太合适了。另外,您如何准确定义术语参考?非正式地,Python 方式可以很容易地描述为传递对象的地址。但它不适合 Python 的潜在分布式实现。
我喜欢这个答案,但你可能会考虑这个例子是真的帮助还是伤害了流程。此外,如果您将“参考值”替换为“对象参考”,您将使用我们可以认为是“官方”的术语,如下所示:Defining Functions
该引用的末尾有一个脚注,内容如下:“实际上,按对象引用调用会是一个更好的描述,因为如果传递了一个可变对象,调用者将看到被调用者对其所做的任何更改...” 我同意您的观点,即混淆是由于试图适应与其他语言建立的术语而引起的。抛开语义不谈,需要了解的有:字典/命名空间、name binding operations 以及名称→指针→对象的关系(如您所知)。
A
AmanicA

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

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

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

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


+1 用于少量文本,为 Python 没有传递引用的问题提供了基本的解决方法。 (作为适合此处以及此页面上任何地方的后续评论/问题:我不清楚为什么 python 不能像 C# 那样提供“ref”关键字,它只是将调用者的参数包装在一个列表中,如this,并将对函数中参数的引用视为列表的第 0 个元素。)
好的。要通过 ref,请用 [ ] 包裹起来。
K
KobeJohn

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

我认为重要的是要注意当前投票最多的帖子(由布莱尔康拉德撰写),虽然就其结果而言是正确的,但它具有误导性,并且根据其定义是不正确的。虽然有许多语言(如 C)允许用户通过引用传递或按值传递,但 Python 不是其中之一。

David Cournapeau 的回答指向了真正的答案,并解释了为什么 Blair Conrad 的帖子中的行为似乎是正确的,而定义却不是。

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

如果你想要这种行为,布莱尔康拉德的回答很好。但是,如果您想了解 Python 既不是按值传递也不是按引用传递的具体细节,请阅读 David Cournapeau 的回答。


并非所有语言都是按值调用的。在 C++ 或 Pascal 中(当然还有许多我不知道的其他语言),您可以通过引用调用。例如,在 C++ 中,void swap(int& x, int& y) { int temp = x; x = y; y = temp; } 将交换传递给它的变量。在 Pascal 中,您使用 var 而不是 &
我以为我很久以前就回复了这个,但我没有看到它。为了完整性 - cayhorstmann 误解了我的回答。我并不是说按照大多数人首先学习的有关 C/C++ 的术语来说,一切都是按价值调用的。只是传递了一些值(值、名称、指针等),并且 Blair 原始答案中使用的术语不准确。
A
Alex L

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

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

是的,但是如果你这样做 x = [ 2, 4, 4, 5, 5], y = x, X[0] = 1 , print x # [1, 4 ,4, 5, 5] print y # [1 , 4, 4, 5, 5]
X[0] 还是 x[0] ?不明白
a
ajknzhol

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

不可变参数有效地“按值”传递。整数和字符串等对象是通过对象引用而不是通过复制来传递的,但是由于无论如何您都无法更改不可变对象,因此效果很像复制。

可变参数有效地“通过指针”传递。列表和字典等对象也通过对象引用传递,这类似于 C 将数组作为指针传递的方式——可变对象可以在函数中就地更改,就像 C 数组一样。


M
Mike Mazur

在这种情况下,方法 Change 中标题为 var 的变量被分配了对 self.variable 的引用,并且您立即将字符串分配给 var。它不再指向 self.variable。以下代码片段显示了如果您修改 varself.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']
>>> 

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


J
Joop

这里有很多关于答案的见解,但我认为这里没有明确提到另外一点。引用 python 文档 https://docs.python.org/2/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-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

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


+1很好的答案,这提到了我的想法。当没有人提到这一点时,我感到非常惊讶。如果没有进行赋值,则别名和原始值为一,但是当我们重新赋值时,链接断开,并在函数内部创建了一个新的本地对象。
i
iliketocode

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

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

我很想发表类似的回复——最初的提问者可能不知道他想要的实际上是使用一个在函数之间共享的全局变量。以下是我要分享的链接:stackoverflow.com/questions/423379/… 作为对 @Tim 的回答,Stack Overflow 不仅是一个问答网站,它还是一个庞大的参考知识库,只会变得更强大、更细致——就像一个活跃的 wiki——更多输入。
C
Community

这是 Python 中使用的概念 pass by object 的简单(我希望)解释。
每当您将对象传递给函数时,对象本身就会被传递(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)

这显然会改变对象。 This answer 很好地解释了这一点。


问题是作业做的事情超出了你的预期。 list = [1, 2, 3] 导致将 list 名称重用于其他内容并忘记最初传递的对象。但是,您可以尝试 list[:] = [1, 2, 3](顺便说一下,list 是变量的错误名称。考虑 [0, 1] = [1, 2, 3] 完全是胡说八道。无论如何,您认为 对象本身已传递? 在你看来,什么是复制到函数中的?
@pepr 对象不是文字。它们是对象。谈论他们的唯一方法是给他们一些名字。这就是为什么一旦你掌握它就很简单,但解释起来却非常复杂。 :-)
@Veky:我知道这一点。无论如何,列表文字被转换为列表对象。实际上,Python 中的任何对象都可以没有名称而存在,即使没有名称也可以使用。您可以将它们视为匿名对象。将对象视为列表的元素。他们不需要名字。您可以通过列表的索引或迭代来访问它们。无论如何,我坚持 [0, 1] = [1, 2, 3] 只是一个不好的例子。 Python 中没有类似的东西。
@pepr:我不一定指 Python 定义的名称,只是普通的名称。当然 alist[2] 算作 alist 的第三个元素的名称。但我想我误解了你的问题。 :-)
啊。我的英语显然比我的 Python 差很多。 :-) 我会再试一次。我只是说你必须给对象一些名字才能谈论它们。我所说的“名称”并不是指“Python 定义的名称”。我知道 Python 机制,不用担心。
i
iliketocode

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

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

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

在实例方法中,您通常参考 self 来访问实例属性。在 __init__ 中设置实例属性并在实例方法中读取或更改它们是正常的。这也是您将 self 作为第一个参数传递给 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

i
iliketocode

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

是的,这也解决了我的用例中的“通过引用”。我有一个函数基本上清除 dict 中的值,然后返回 dict。然而,在清理过程中,很明显需要重建系统的一部分。因此,该函数不仅必须返回已清理的 dict,而且还必须能够发出重建信号。我试图通过引用传递 bool,但 ofc 不起作用。弄清楚如何解决这个问题,我发现您的解决方案(基本上返回一个元组)效果最好,同时也根本不是黑客/解决方法(恕我直言)。
@kasimir 这是我真正喜欢 Python 的原因之一。因为将多个值作为元组返回非常容易,所以很少需要通过引用传递。
@MarkRansom 我也是!我做了很多 PHP 编程,通过引用传递在那里很常见,但在尝试调试时可能会很痛苦。 Python 可以让你避免这种情况,所以我更喜欢 Python 的另一个原因 :-)
i
itmuckel

通过引用传递对象有一个小技巧,即使语言无法实现。它也适用于 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 所指向的同一个列表对象。但是因为列表是可变的,所以对列表的更改对于 both 引用都是可见的。在本例中,您使用 ref 引用来更改索引 0 处的对象,以便它随后存储 PassByReference('Michael') 对象。对列表对象的更改是使用 ref 完成的,但此更改对 p 可见。
所以现在,引用 pref 指向存储单个对象 PassByReference('Michael') 的列表对象。因此,p[0].name 返回 Michael。当然,ref 现在已经超出范围,可能会被垃圾回收,但都是一样的。
但是,您没有更改与引用 obj 关联的原始 PassByReference 对象的私有实例变量 name。事实上,obj.name 将返回 Peter。上述评论假定定义 Mark Ransom 给出。
重点是,我不同意这是一个 hack (我认为它的意思是指一些可行的东西,但由于未知、未经测试或实施者无意的原因)。您只需将列表中的一个 PassByReference 对象替换为另一个 PassByReference 对象,并引用这两个对象中的后者。
m
mARK bLOORE

鉴于 python 处理值和对它们的引用的方式,您可以引用任意实例属性的唯一方法是通过名称:

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

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

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


J
Jesse Hogan

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

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'

虽然这行得通。它不是通过引用传递的。它是“通过对象引用传递”。
D
Daniel Jour

由于似乎没有提到模拟引用的方法,例如 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

这对于“仅输出引用”或具有多个线程/进程的情况(通过使更新函数线程/多处理安全)非常有用。

显然上面不允许读取值,只能更新它。


L
Liakos

由于字典是通过引用传递的,因此您可以使用 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!"
    return result

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 returned value
print("multi: ", ref['multi'])  # a referenced value
print("msg: ", ref['msg'])      # a referenced value

t
textshell

虽然通过引用传递并不适合python并且应该很少使用,但实际上有一些变通方法可以将当前分配给局部变量的对象,甚至从被调用函数内部重新分配局部变量。

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

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

def change(wrapper):
    wrapper(7)

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

同样的想法也适用于读取和del变量。

仅仅为了阅读,还有一个更短的方法是使用 lambda: x,它返回一个可调用的,当被调用时返回 x 的当前值。这有点像在遥远的过去语言中使用的“按名称呼叫”。

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

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


s
sergzach

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

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)

h
haidi ju

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

Java&C#:原始类型(包括字符串)按值传递(副本),引用类型按引用传递(地址副本),因此调用函数中对参数所做的所有更改对调用者都是可见的。

C++:允许按引用传递或按值传递。如果参数是通过引用传递的,您可以根据参数是否作为 const 传递来修改或不修改它。但是,无论是否为 const,参数都维护对对象的引用,并且不能将引用分配为指向被调用函数内的不同对象。

Python:Python 是“pass-by-object-reference”,也就是常说的:“对象引用是按值传递的。”[阅读这里]1.调用者和函数都引用同一个对象,但函数中的参数是一个新变量,它只是在调用者中保存对象的副本。与 C++ 一样,参数可以在函数中修改或不修改 - 这取决于传递的对象类型。例如;不可变对象类型不能在被调用函数中修改,而可变对象可以更新或重新初始化。更新或重新分配/重新初始化可变变量之间的一个关键区别是更新的值会反映在被调用的函数中,而重新初始化的值不会。将新对象分配给可变变量的任何范围对于 python 中的函数都是本地的。 @blair-conrad 提供的示例非常有助于理解这一点。


老了,但我觉得有必要纠正它。字符串在 Java 和 C# 中都是通过引用传递的,而不是通过值传递
不,一切都是在 c# 中按值传递的。在c#中作为对象的变量的值正是该对象的堆ID/地址。因此,当您将函数中的某些内容设置为新对象时,您将函数中的变量设置为地址。通过引用传递意味着将地址传递给值,该值是结构类型的值的地址,但在对象的情况下是指针的地址。
我知道当您说地址副本(为什么存在 ref)时您是专家,它通过引用它自己而不是 C# 中的副本
M
Magnus

我是 Python 新手,从昨天开始(尽管我已经编程了 45 年)。

我来到这里是因为我正在编写一个函数,我想要两个所谓的输出参数。如果它只是一个输出参数,我现在不会挂断检查引用/值在 Python 中的工作方式。我只会使用函数的返回值。但是由于我需要两个这样的输出参数,所以我觉得我需要对其进行整理。

在这篇文章中,我将展示我是如何解决我的情况的。也许来到这里的其他人会发现它很有价值,即使它并不完全是主题问题的答案。经验丰富的 Python 程序员当然已经知道我使用的解决方案,但它对我来说是新的。

从这里的答案中,我可以很快看到 Python 在这方面的工作方式有点像 Javascript,如果你想要参考功能,你需要使用变通方法。

但是后来我在 Python 中发现了一些我认为我以前在其他语言中没有见过的简洁的东西,即你可以从一个函数中返回多个值,以简单的逗号分隔方式,如下所示:

def somefunction(p):
    a=p+1
    b=p+2
    c=-p
    return a, b, c

并且您可以类似地在调用方处理它,就像这样

x, y, z = somefunction(w)

这对我来说已经足够好了,我很满意。无需使用一些解决方法。

在其他语言中,您当然也可以返回许多值,但通常在对象的 from 中,您需要相应地调整调用方。

Python 的做法很简单。

如果您想通过引用来模仿更多,您可以执行以下操作:

def somefunction(a, b, c):
    a = a * 2
    b = b + a
    c = a * b * c
    return a, b, c

x = 3
y = 5
z = 10
print(F"Before : {x}, {y}, {z}")

x, y, z = somefunction(x, y, z)

print(F"After  : {x}, {y}, {z}")

这给出了这个结果

Before : 3, 5, 10  
After  : 6, 11, 660

“但后来我在 Python 中发现了一些我认为我以前在其他语言中没有见过的简洁的东西,即你可以从一个函数返回多个值”不,你不能。您所做的是返回一个值 tuple,它是表达式 a, b, c 创建的。然后,您使用 iterable unpacking 将该元组解包为单独的变量。当然,实际上,您可以将其视为“返回多个值”,但您实际上并没有这样做,您正在返回一个容器。
@juanpa.arrivillaga,是的,我在写答案时就知道,我刚刚读过它。但是我只是以实用的方式描述了整个事情,而没有详细说明它是如何工作的,并且在我的答案中增加了不必要的长度。您确实可以从函数返回多个值,如果它是在对象或类似对象中完成的,例如在元组中(在 Python 中以我展示的简洁方式处理)。当我从一家公司订购东西时,他们可以寄给我多件东西,即使它们都在一个包裹中。
J
Julian wandhoven

或者你可以使用 ctypes 女巫看起来像这样

import ctypes

def f(a):
    a.value=2398 ## resign the value in a function

a = ctypes.c_int(0)
print("pre f", a)
f(a)
print("post f", a)

因为 a 是 ac int 而不是 python 整数,并且显然是通过引用传递的。但是你必须小心,因为可能会发生奇怪的事情,因此不建议


J
Jop Knoppers

很可能不是最可靠的方法,但这是可行的,请记住,您正在重载内置的 str 函数,这通常是您不想做的事情:

import builtins

class sstr(str):
    def __str__(self):
        if hasattr(self, 'changed'):
            return self.changed

        return self

    def change(self, value):
        self.changed = value

builtins.str = sstr

def change_the_value(val):
    val.change('After')

val = str('Before')
print (val)
change_the_value(val)
print (val)

S
Stepan Dyatkovskiy

dataclasses 呢?此外,它还允许您应用类型限制(又名“类型提示”)。

from dataclasses import dataclass

@dataclass
class Holder:
    obj: your_type # Need any type? Use "obj: object" then.

def foo(ref: Holder):
    ref.obj = do_something()

我同意人们在大多数情况下最好考虑不使用它的观点。

然而,当我们谈论 contexts 时,了解这种方式是值得的。

您可以设计显式上下文类。在进行原型设计时,我更喜欢数据类,只是因为很容易来回序列化它们。

干杯!