本文译自 Is Python pass-by-reference or pass-by-value?

“假设我对 Fat 说,或 Kevin 对 Fat 说,“你没有经历过上帝。你只不过经历了一些与上帝的品质、方面、性质、力量、智慧和善良有关的事情。”这就像是关于德国人讲的一个双重抽象倾向的笑话;德国英国文学权威宣称,“哈姆雷特不是莎士比亚写的;它只是由一个名叫莎士比亚的人写的。”在英语语境中,这句话的区别只是口头的,没有实际意义,尽管德语中这种表达存在差异(这解释了德国思想的一些奇怪特征)。”

–Valis,p71(Book-of-the-Month-Club Edition)

Philip K. Dick 并不以其轻松或易懂的散文而闻名。绝大多数角色都很高。就像,真的,真的,真的很高。然而,在 Valis 的上述引文(1981 年出版)中,他对臭名昭着的Python参数传递范式给出了非常有远见的解释。越是变化,越是全能。(Plus ça change, plus c’est omnomnomnom drugs.)

在编程语言中参数传递的两种最广为人知且易于理解的方法是按引用传递( pass-by-reference )和按值传递 ( pass-by-value )。不幸的是,Python是“传递对象引用”( pass-by-object-reference ),经常说:

“对象引用按值传递。”(Object references are passed by value.)

当我第一次看到这个沾沾自喜和过于精辟的定义时,我想捶人。在从手上取下玻璃碎片并被护送出脱衣舞俱乐部后,我意识到,可以通过以下两个函数的行为方式来理解以上这三种范式:

def reassign(alist):
alist = [0, 1]

def append(alist): # 该函数命名不规范
alist.append(1)

alist = [0]
reassign(alist)
append(alist)

让我们来一探究竟。

变量不是对象

“哈姆雷特不是莎士比亚写的;它只是由一个名叫莎士比亚的人写的。” Python 和 PKD( Philip K. Dick)都在一个东西的本质与我们用来指代那个东西的标签之间做出了至关重要的区分。 “这个名叫莎士比亚的男人”是一个具体的人。 而“莎士比亚”只是一个名字。如果我们这样做:

a = []

[]是一个空列表。 a 是指向空列表的变量,但其本身并不是空列表。我画图并将变量称为包含对象的“盒子”;但无论如何你构想它,这种差异是关键。
Pass-by-reference

通过引用传递

在 pass-by-reference 中,box(变量)直接传递给函数,其内容(由变量表示的对象)隐性地随之而来。在函数上下文中,参数本质上是调用者传入的变量的完整别名。它们都是完全相同的盒子,因此也指向内存中完全相同的对象。

因此,函数对变量或它所代表的对象所做的任何操作都将对调用者可见。例如,该函数可以完全更改变量的内容,并将其指向完全不同的对象:

该函数还可以在不重新分配对象的情况下操作对象,效果相同:

重申一下,在pass-by-reference中,函数和调用者都使用完全相同的变量和对象。

通过值传递

pass-by-value中,函数接收调用者传递给它的参数对象的副本,并在内存中开辟新的空间保存。

然后,该函数有效地提供其自己的盒子以将值放入,并且函数和调用者引用的变量或对象之间不再存在任何关系。这些对象碰巧具有相同的值,但它们完全是分开的,一个对象不会影响到另一个。如果我们再次尝试重新分配:

在函数之外,没有任何反应。同理:

调用者上下文中的变量和对象的副本是完全隔离的。

通过对象引用传递

Python是不同的。众所周知,在Python中,“对象引用按值传递”(Object references are passed by value)。

函数接收对(并将访问)内存中与调用者使用的相同对象的引用。但是,它不会收到调用者正在存储此对象的盒子;在pass-by-value中,函数提供自己的筐并为自己创建一个新变量。让我们再次执行append

函数和调用者都引用内存中的同一个对象,所以当append函数向列表中添加一个额外的项时,我们也会在调用者中看到这个!它们是同一个东西的不同名称;包含相同对象的不同筐。这意味着是通过值传递对象引用的——函数和调用者在内存中使用相同的对象,但是通过不同的变量访问。这意味着同一个对象被存储在多个不同的筐中,而这种隐喻会被打破。假定它是量子或其他东西。

但关键是它们真的是不同的名字和不同的盒子。在pass-by-reference中,它们是相同的盒子。当你试图重新分配一个变量,并将一些不同的东西放入函数的盒子中时,你也将它放入调用者的盒子中,因为它们是同一个盒子。但是,在pass-by-object-reference中:

调用者不在乎你是否重新分配方法的盒子。不同的盒子,相同的内容。

现在我们看看菲利普·K·迪克试图告诉我们的事情。名字和人是不同的东西。变量和对象是不同的东西。有了这些知识,你或许可以开始推断当你做这样的事情时会发生什么

listA = [0]
listB = listA
listB.append(1)
print listA

你可能还想了解这些概念与可变和不可变类型之间的有趣交互。但这些是另一回事了。现在,如果你能原谅我,我要去读《仿生人会梦见电子羊吗?》(《Dororoids Dream Of Electric Sheep?》)了。 ——我对元编程有点生疏。

其他语言有’variables’,而 Python 有’names’

其他语言

在许多其他语言中,给变量赋值可以看做是把一个值放入一个盒子中。如:

int a = 1

将另一个值赋值给同一个变量可以看成是盒子里面换了个内容,如:

int a = 2

盒子“a”里面现在是整数 2。

将一个变量赋值给另一个变量会复制该值并将其放入新盒子中:

int b = a;

“b”是第二个盒子,有一个整数 2 的副本。a 盒子有一份单独的副本。

Python

在 Python 中,“名称”或“标识符”就像附加到对象上的包裹标签(或名称标签)一样。

a = 1


如果我们给“a”重新赋值,我们只需要将标签移动到另一个对象:
a = 2

现在名称“a”被附加到一个整数 2 对象上。

原始的整数 1 对象不再有标记“a”。它可能会继续存在,但我们不能通过名字 a 找到它。(当一个对象没有更多的引用或标记时,它将从内存中删除。)

如果我们将一个名称赋给另一个名称,我们只是将另一个名称标签附加到一个现有的对象上:

b = a


名称“b”只是绑定到与“a”相同的对象的第二个标签。

尽管我们甚至在 Python 中也经常使用“变量”(因为这是通用术语),但实际上我们的意思是“名称”或“标识符”。 在 Python 中,“变量”是值的名称标签,而不是带标签的盒子。

参阅 Code Like a Pythonista: Idiomatic Python

推荐阅读