Python进阶之标识与别名

前言

在python中,原本一句简简单单的赋值语句,竟然也可以牵扯出复杂的内存空间的关系。

为什么这么说呢? 快来看看今天对python标识、别名的剖析吧。


变量名不是盒子

“变量名不是盒子,而是标识” 这句话形象的揭露了变量的本质。

举个栗子:

上图中,我们将 a 的列表赋值给变量b,发现 b 的内容也发生了改动。

为什么会这样呢?

虽然我们用 “变量存储着可变的数据” 概括了变量的作用。

但我们不能理解为“变量是个盒子,装着数据”;而是要将其理解为 “存储的数据指向了这个变量名”。

说着有点拗口,看图吧!

  • 所以,a跟b都指向了同一块地址,b 也能看出内部数据被修改的痕迹咯。
  • 为了理解 Python 中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。



别名

通过等号赋值,当一个变量赋值给另一个变量后,就可以说有了多个别名。

别名相当于多个标识指向了同一片数据,is 运算符和 id 函数确认了这一点。

从各个别名上对此所绑定的对象上做出的改动,都会互相影响到对方

  • is 运算符比较两个对象的标识;
  • id() 函数返回对象标识的整数表示。



新对象绑定与浅复制

如果是重新定义的对象绑定,就不是同一个标识了。什么意思呢? 我们可以理解为,使用的是新创建的对象、或者说使用浅复制的对象绑定,就不是同一个标识。

  • my_list3 和 new_list 虽然内容和 my_list1 一样,但是指向的并不是同一片内存地址。



共享传参的后果

Python 唯一支持的参数传递模式是共享传参(call by sharing)

共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名

这样会导致:函数可能会修改作为参数传入的可变对象,不过无法修改那些对象的标识(即不能把一个对象替换成另一个对象)

  • 发现不可变类型的数据类型定义的值不会发生改变;

  • 而可变的数据类型定义的值经过函数运行后发生改变。


Ps:之所以导致这种情况,是因为整数的+=自增操作符和列表的区别。之前有说过 Python进阶之序列的其他姿势




不要使用可变类型作为参数的默认值

将可变参数当作默认值可能还会导致一系列问题,我们举个栗子看看吧:

1
2
3
4
5
6
7
8
9
class HauntedBus:
def __init__(self, passengers=[]):
self.passengers = passengers

def pick(self, name):
self.passengers.append(name)

def drop(self, name):
self.passengers.remove(name)

功能很简单,创建一个类,其构造函数默认是一个列表。

功能函数有两个,一个是增加传进去的参数到列表中,另一个是从列表中删除。

一切都如我们想象的那么美好,但实际情况却是这样的:

哈?bus2竟然有两个参数??明明我们只是添加了一个啊…


让我们看看究竟是什么原因:

这是因为 self.passengers 变成了 passengers 参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。

另一个使用默认值的空列表时,它就不会重新创建一个新列表,而是使用已经创建过的列表进行初始化。

因此,如果默认值是可变对象,而且做出了修改,那么后续的函数调用都会受到影响。


所以,我们知道了,最好不要使用可变参数作为默认初始值,或者保证初始化的列表都是新的空列表。



所以我们应该这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NewBus:

def __init__(self, passengers=None):
if passengers is None: # 如果,默认值为空,即不传参数,就生成一个空列表
self.passengers = [] # 确保了创建的是新的空列表
else:
# passengers = passengers
# 这么写会导致“别名”,也就是说,会更改外部传进来的列表(上面有说)
self.passengers = list(passengers) # 改为传入一个副本

def pick(self, name):
self.passengers.append(name)

def drop(self, name):
self.passengers.remove(name)

效果验证




写在最后

我认为在python中,引用的概念是很重要的。弄清楚什么是别名、什么是标识,有利于识破python的“魔法”

okk,此篇就讲到这里了,拜拜!