
你是否在编写Python时遇到过这样的错误:你以为自己在修改一个局部变量,但实际上也在函数之外修改了这个变量?
例如,如果你有一个这样的函数。
def add_one(x: int, y: list) -> None:
x += 1
y += [1]
然后当你调用它时
a = 0
b = []
add_one(a, b)
你发现a = 0,但是b = [1]。
在这篇文章中,我们将探讨导致这种行为的原因,这样你就可以避免在代码中遇到奇怪的错误。
可变的和不可变的
理解这种行为的第一步是了解可变和不可变类型在赋值过程中的行为方式的区别。
在 Python 中,我们有可变和不可变的数据类型,这意味着你可以修改某些数据类型的值,而你不能修改其他数据类型的值。
可变类型的例子有
- 字典
- 列表
- 集合
- 类
而不可变的类型的例子有
- 字符串
- 整数
- 浮点数
- 布尔类
- 元组
拥有可变的数据类型意味着我们可以修改它们,并且仍然拥有相同的对象,而如果我们试图修改不可变的类型,我们就会创建一个新的对象,为了能够在幕后证明这一点,我们将使用内置的id()方法,它给我们对象的唯一标识符。如果两个对象有相同的标识符,那么它们引用的是同一个对象,而如果标识符不同,那么它们引用的是不同的对象。
我们可以通过比较一个可变的数据类型(list)和一个不可变的数据类型(int)来看看这个动作。
x = 0
y = []
id(x) # 4353643616
id(y) # 4356958960
x += 1
y += [1]
id(x) # 4353643648
id(y) # 4356958960
我们在这里可以看到,在给x添加1后,它目前引用的是值0,现在引用的是一个新对象,即整数1。反之,我们的y列表仍然引用同一个对象。
因此我们可以看到,如果我们试图修改一个不可变的数据类型,我们实际上引用了一个新的对象,而对于一个可变的数据类型,我们保持对同一个对象的引用,而是直接修改对象的内容。
通过赋值传递
现在我们已经看到了当你对它们进行操作时,易变和不可变对象的工作方式的不同,让我们来理解谜题的另一部分,它决定了为什么我们可以像我们之前看到的那样在一个函数中修改易变对象。
这种行为是由于 Python 处理向函数传递参数的方式,这是通过一个叫做 "通过赋值传递 "的原则完成的。通过赋值传递意味着我们以处理变量赋值的相同方式来传递变量,比如说。
a = 1
b = a
b is a # True
a += 1
b is a # False
这里发生的事情是,我们已经把 a 赋值给了整数对象 1,然后我们把 b 赋值给了 a,这意味着 b 现在和 a 引用的是同一个对象。
如果我们修改a,给它加上1,因为我们知道整数是不可改变的,a现在引用了一个新的对象,而b仍然引用旧的对象,因此它们不再是引用同一个对象了。
现在知道了赋值的工作原理,如果我们再看一下我们的列表例子。
x = []
y = x
x is y # True
x += [1]
x is y # True
print(x) # [1]
print(y) # [1]
我们在这里有一个例子的原则,x和y引用了同一个对象。然而,由于 list 是一个可变的对象,修改它并不会导致创建和引用一个新的对象,相反,我们保持同一个对象的引用,只是修改对象的内容。
将这一切结合起来
现在我们理解了可变和不可变对象的区别,以及赋值在 Python 中是如何工作的,让我们来理解当我们从上面调用 add_one 函数时实际发生了什么。
为了避免大量的滚动,让我们再一次把我们的玩具例子复制在这里
def add_one(x: int, y: list) -> None:
x += 1
y += [1]
a = 0
b = []
add_one(a, b)
print(a) # 0
print(b) # [1]
在创建了这个函数之后,我们做的第一件事是将 a 指向整数对象 0,将 b 指向一个空的 list 对象。
接下来我们用a和b调用函数,这意味着x被分配了a的值,y被分配了b的值。
现在我们在函数中修改x,给它加1。因为我们的整数是不可改变的,x被分配为引用新的整数对象。
接下来,我们将元素1添加到我们列表的末尾。在这种情况下,由于我们的列表是可变的,我们不创建一个新的对象,而是更新现有的对象。
现在我们已经到达了函数的末尾,离开了它的范围,x 和 y 不再存在,因为它们是 add_one 函数范围内的局部变量。我们的值a从未被更新,因为它所引用的对象从未改变(因为它是不可变的)。另一方面,我们的值b确实被改变了,因为它所引用的对象被改变了。
总结
Python 的内部结构非常强大,允许非常灵活的代码。然而,如果我们不完全理解幕后发生的事情,有时我们会被它们的行为所吓倒。这可能会导致你的程序中出现奇怪的错误,而这些错误是很难追踪到的。
了解Python的赋值操作是如何工作的,以及可变和不可变的数据类型在修改时的行为是如何不同的,这意味着你不仅可以避免在你的程序中错误地出现这些 "bug",而且实际上你可以利用这些行为来编写更干净、更简洁的Python代码。