Python是当前使用最方便快捷的编程语言。作为一个使用Python的程序员,理解内部机制是写好代码的基础。其中,首当其冲的就是最基础的变量,以及其背后的内存引用机制。

Python引用机制

Python中的变量内存机制类似C++中的引用,即变量是一份内存的引用,每个变量不一定都占据一份单独的内存空间,可能有多个变量对应同一个内存空间的情况。这里我们可以通过用变量赋值和用值赋值两种情况来进一步理解

  1. 使用变量为变量赋值:例如b=a,用变量赋值,不用关心变量的值是什么,传的是引用,将变量a的引用传给了b,因此a和b有相同的地址
  2. 使用值为变量赋值:例如a=257, b=257, 用值赋值,需要单独分配内存,为b分配了一块内存,内存里的值为257,因此a和b的地址不同
>>> a = 257; b = a  
>>> id(a) == id(b)  #传的是a的引用,地址与a相同
True 
>>> a = 257; b = 257  
>>> id(a) == id(b)  #单独分配了一块内存,值为257,地址与a不同
False



这里大家会有个疑问,为什么举例使用的是整型的257。如果使用整型其他值或字符串浮点等类型会发生什么呢?不妨来试一下



>>> a = 1; b = 1
>>> id(a) == id(b)
True
>>> a = 1.1; b = 1.1
>>> id(a) == id(b)
False 
>>> a = "1"; b = "1"
>>> id(a) == id(b)
True



根据上面的尝试,可以看出整型1和字符串类型都没有单独分配内存,相同值的变量实际上是同一块内存的引用。而浮点类型则是单独分配了内存。

这里涉及到Python缓存小数据的机制,即为了性能方面的考虑,对整型[-5, 256]和字符串类型进行缓存,当初始化这部分值的变量时,不再单独分配内存。

Python中的不可变数据类型与可变数据类型

先看定义,这里讨论的都是python的基本数据类型,自定义类都属于可变类型:

  1. 不可变数据类型:值更改后地址发生变化,包括数字、字符串、元组
  2. 可变数据类型:值改变后地址不发生变化,包括列表、字典、集合

对不可变类型,比较好理解,数字和字符串值变化了,或者是变量引用指向一个新的地址,或者是根据变化后的值新分配一块内存,变量的引用指向这块内存。此外元组本身就定义为不可变,值发生变化只能是重新定义。对可变类型,可以看到这三种类型都是由元素组合而成,元素的变化或增减都不改变类型本身的引用。



>>> a = 1; b = a
>>> id(a) == id(b)
True
>>> b = 2
>>> id(a) == id(b)  #不可变数据类型,值变地址变
False
>>> 
>>> a = [1,2,3]; b = a
>>> id(a) == id(b)
True
>>> b[0] = 11
>>> a
[11, 2, 3]
>>> id(a) == id(b)  #可变数据类型,值变地址不变
True



需要说明的是,在函数传参时,传递的也是引用,和可变不可变数据类型无关。在有些技术博客上会出现这样的叙述:

python不允许程序员选择采用传值还是传引用。Python参数传递采用的肯定是“传对象引用”的方式。实际上,这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值——相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象——相当于通过“传值’来传递对象。

这段叙述中,后半段的结论没问题,但是前半段的叙述不够准确,很容易让读者产生误解。Python参数传递实际上是传递引用,这和可变和不可变数据类型没有关系。“传值”的含义在于分配一块新的内存空间,而函数传参并不一定符合这一点。因此对于可变和不可变类型,理解的重点在于值的变化,而不在于传递



>>> def func(a):
	return a
>>> a = 1
>>> b = func(a)   #传参并不一定是传值
>>> id(a) == id(b)
True



因此,对于可变数据类型,在赋值和传参时,我们需要特别注意 值的修改会反映到所有的引用上。多个变量指向同一个list时,任何一个变量对list值进行改动,都会影响其他变量。这点我自己也是踩坑多次。对于这种情况,我们可以采用浅拷贝的方式为变量单独分配内存。下面是几个踩坑的例子。



#直接用乘n的方式生成的是多个引用,地址是相同的
>>> a = [[1,2,3]] * 3  
>>> a
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
>>> a[0][0] = 11
>>> a
[[11, 2, 3], [11, 2, 3], [11, 2, 3]]
>>> [id(_) for _ in a] 
[2325771410696, 2325771410696, 2325771410696]

#传参传的也是引用,class里可能会有多次赋值,难以察觉
>>> class D:
	def __init__(self, lst):
		self.lst = lst
	def getFirst(self):
		return self.lst.pop(0)
>>> a = [1, 2, 3]
>>> d = D(a)
>>> d.getFirst()
1
>>> a   #a, d.lst是同一对象的两个引用
[2, 3]

#两种浅拷贝方式
>>> a = [1,2,3]
>>> b = a.copy()
>>> id(a) == id(b)
False
>>> c = a[:]
>>> id(a) == id(c)
False



总结

  1. Python变量是一份内存的引用
  2. 变量赋值时,如果等号右侧是个引用,则直接传递引用;如果等号右侧是值,则新分配一块内存。整型[-5, 256]和字符串类型是特例,有缓存小数据机制,不单独分配内存。
  3. 可变与不可变数据类型的区别在于值变化时引用是否变化,即是否重新分配内存。可变类型包括列表、字典和集合;不可变类型包括数字、字符串、元组。
  4. 对于列表、字典和集合,在赋值和传参时一定要多加注意,防止同引用变量的修改造成错误,保险的做法是赋值时采用浅拷贝的方式。