0、 参考文档
参考文档如下:
- stackoverflow: How do I pass a variable by reference?
- python官方文档: How do I write a function with output parameters (call by reference)?
1、 引言
对于没有接触过其他语言的python开发人员来说,似乎从来没有按值传递与按引用传递这个概念。但是如果你已经学过C++等其他语言,你心中就难免有疑惑,python中函数传递时候,是采用值传递还是引用传递??这会是一个巨大的问题。
我以前也从来没意识到这个问题,直到看到了《python参考手册》第四版这本书中的介绍:
调用函数时,函数参数仅仅是指代传入对象的名称。参数传递的基本语义和其他编程语言中已知的方式不完全相同,如“按值传递”或“按引用传递” 。例如,如果传递不可变的值,参数看起来实际是按值传递的。但如果传递可变对象(如列表或字典)给函数,然后再修改此可变对象,这些改动将反映在原始对象中。
书中给的示例代码【示例代码1-1】:
a = [1, 2, 3, 4, 5]
def square(items):
for i, x in enumerate(items):
items[i] = x * x # 原地修改item中的元素 square(a)
square(a)
print(a)
执行的结果:
[1, 4, 9, 16, 25]
这会给我们的编程造成巨大的潜在风险,所以要非常小心的规避这种风险。 书中作者也写了:
调像这样悄悄修改其输入值或者程序其他部分的函数被认为具有副作用。
一般来说,最好避免使用这种编程风格,因为随着程序的规模和复杂程度不断增加,这类函数会成为各种奇怪编程错误的根源(例如,如果函数具有副作用,只看函数调用是无法明显发现的) 。
在涉及线程和并发的程序中,这类函数的交互能力很差,因为通常需要使用锁定来防止副作用的影响。
2、 如何解决这个问题
很明显,这是一个通用的问题,在stackoverflow中有着非常多的关注与解答,链接如下:
我将这个问题与最佳的解答进行翻译,并对解答中的内容进行简单的加工, 并给出了自己的建议。
2.1 原始的提问问题
python中的参数是通过引用传递(passed by reference) 还是通过值传递(passed by value) 的?如何通过引用传递(passed by reference) ,以便下面的代码输出“Changed”而不是“Original”?
class PassByReference:
def __init__(self):
self.variable = 'Original'
self.change(self.variable)
print(self.variable)
def change(self, var):
var = 'Changed'
如果实例化这个类,可以看到打印的结果是"Original"而不是"Changed"。
2.2. 这个问题的总体思想
python参数是通过分配传递 passed by assignment 的。这背后的理由是双重的:
- 传递的参数实际上是对对象的引用(reference),但引用由值传递(the reference is passed by value)
- 一些数据类型是可变的(mutable),但另一些则不是
所以:
- 如果您将可变对象(a mutable object) 传递给方法,该方法将获得对该对象的引用(reference),您可以根据自己的心情对其进行进行改变,但如果您在方法中重新绑定(rebind)引用(reference),外部范围将对此一无所知,完成后,外部引用(outer reference)仍将指向原始对象。
- 如果您将不可变对象(an immutable object) 传递给方法,您仍然无法重新绑定(rebind)外部引用(outer reference),同时无法对对象进行改变。
为了更清楚地说明,让我们举几个例子。
2.3. List - 可变的类型(a mutable type)
2.3.1. 对于修改List的情况1:
让我们尝试修改传递给方法(即函数)的列表【示例代码2-1】:
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)
Output:
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
的引用(reference),而不是它的副本,我们可以使用修改列表的方法来更改它,并将更改反映在外部范围(the outer scope)中。
这个结果与我们最初的例子是相通的,大家也都很容易理解为什么。
2.3.2. 对于修改List的情况2:
现在让我们看看: 当我们尝试更改作为参数传递的引用(reference)时会发生什么【示例代码2-2】:
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)
Output:
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
参数按值传递(passed by value),因此为其分配的新列表不会影响方法外的代码。the_list
是outer_list
引用的副本,我们让the_list
指向一个新列表,但无法更改outer_list
指向的位置。
我个人的想法:
我个人觉得作者说的不太对,我于是是跳过了他这段的解释,我自己的理解是:
- 对于可变对象,不管什么情况,函数传递的都是对对象的引用。
- 在这个函数内部有一个赋值语句,我把这个赋值语句理解为rebind,rebind就像是在函数内部新创建了一个临时对象,并将"the_list"这个标签贴到临时对象上
- 后续使用"the_list"这个标签对临时对象进行了一些操作,但是不管对这个临时对象怎么操作,都不会影响外面的“outer_list”对象(两个都不是同一个对象,自然没有影响)
2.4. String - 不可变的类型(an immutable type)
String它是不可变的(immutable),所以我们无法改变字符串(String)的内容.
现在,让我们试着更改引用(reference)【示例代码2-3】:
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)
Output:
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
参数是通过值传递的(passed by value),因此为其分配新字符串不会影响方法外的代码。the_string
是outer_string
引用的副本,我们有the_string
指向新字符串,但无法更改outer_string
指向的位置。
我个人的理解:
如果你这么理解:不可变对象,不管什么情况,函数传递的也都是对对象的引用。
因为不可变对象本身是不可变的,所以在函数里面操作的都是肯定都是临时对象(要么是在外面的入参的基础上重新构造,要么重新绑定一个完全不相关的对象)
自然对临时对象的所有操作与外面的不可变对象一点点关系都没有
2.5. 我怎么来实现通过引用传递参数【来自原作者的想法】?
方法1: 你可以在函数中返回新值。这不会改变东西的传递方式,但确实可以让您获得所需的信息。
【示例代码2-4】:
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)
我个人的想法:
对于这种方法, 我还是比较认同的
如果您真的想避免使用返回值,您可以创建一个类来保存您的值并将其传递到函数中(或使用现有类),例如列表。
【示例代码2-5】:
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])
我个人的想法:
我不是很赞同作者的方式,觉得太过于麻烦了,完全破坏了美感。
3. 我自己的想法
上面已经通过多种场景进行阐述:如果把python的函数传递全部理解为传递引用,那么所有的都可以解释通了。
对于书中给的【示例代码1-1】, 对于可变对象,我就是想要值传递,而不是引用传递,那应该怎么做呢:
from copy import deepcopy
a = [1, 2, 3, 4, 5]
def square(items):
_temp_items = deepcopy(items)
for i, x in enumerate(_temp_items):
_temp_items[i] = x * x
print(f"{_temp_items = }")
square(a)
print(f"{a = }")
执行结果:
_temp_items = [1, 4, 9, 16, 25]
a = [1, 2, 3, 4, 5]