首先明确几个概念

变量

所谓变量,是与常量相对的可以改变的量。简单来说,变量是到对象内存空间的一个指针,是一个系统表的元素,拥有指向对象的连接的空间。

Python是弱类型的语言,不像其它强类型的变量(在强类型语言中,变量都是有具体的类型来限制的,规定一个类型的变量只能被赋值与该类型相同或兼容的值),Python中的变量只是个名字,它本身没有数据类型,所以不需要提前声明,也不需要指定类型,只需要在用的时候,给变量赋值即可(使用之前必须赋值),且可以被赋值给任何对象。在给变量赋值时,其实是将被赋值的对象地址存放在了变量中。

更形象一点,变量可看作视对象的一个临时「标签」,是引用,而不是装有对象的容器。

例如:

  • 门外挂着“张先生的房间”的门牌;
  • 矿泉水瓶身上贴的一张写着“刘女士的矿泉水”的标签;

给一个变量赋值,把这个变量名称作为一个标签贴到一个对象上,而重新赋值,是撕下标签贴到另一个对象上。

变量创建

一个变量(也就是变量名),比如a,当代码第一次给它赋值时就创建了它。之后的赋值将会改变该变量名的值。

变量赋值的本质是创建引用,格式为:变量 = 对象

  • 上述格式中定义变量的’=’ 与数学中的’=’ 意义不同。Python 中的等号左边是变量名字,右边是对象值,’=’ 表示将右边的值传递给左边。所以 ‘=’ 是右连接性,对象值的传递顺序是从右到左;
  • 赋值操作符号“=”的作用是,将对象引用和内存中的某个对象进行绑定。如果对象已经存在,就进行简单的重新绑定,以便引用“=”右边的对象;如果对象引用尚未存在,就首先创建对象,然后将对象引用和对象进行绑定。类似于c语言中指针的概念。

具体的,输入:

a = 'python'

代表已经完成了三件事:

  1. 创建了一个对象’python’(分配一块内存,来存储值 ‘python’)
  2. 创建了一个名称为a的变量
  3. 将变量a与对象’python’通过指针建立连接,从变量到对象的连接称之为引用(变量引用对象)

变量类型

类型的概念是存在于对象中而不是变量名中。变量本身是通用的,它只是在一个特定的时间点,简单的引用了一个特定的对象而已。

动态类型机制:Python使用“动态类型”机制,变量不需要提前声明,无需指定其数据类型,其表现完全是动态的,其所为的数据类型决定于当前该变量所引用的对象的数据类型。在Python程序中,支持某个对象引用在任何时候可以重新绑定到另外一个不同的对象上(不要求是相同的类型),这和其他强化型语言不一样(如C++、Java,只允许重新绑定相同类型的对象上)。在Python中,因为有“动态类型”机制,所以一个对象引用可以执行不同类型的对象适用的方法。当一个对象不存在任何对象引用的时候,就进入了垃圾收集的过程。(Python自动回收机制)

a=10
a='122'
a=[1,2,3]

如上例,a在不同的赋值代码行中,引用的对象类型不同,相当于在不断改变a引用的对象。

变量使用

当变量出现在表达中时,它会马上被当前引用的对象所代替,无论这个对象是什么类型。此外,所有变量必须在其使用前明确的赋值,使用未赋值的变量会报错。

对象

对象是分配的一块内存,有足够的空间去表示他们所代表的的值。每一个对象都有两个东西,对象类型和引用的计数器,类型记录了该对象的数据类型,引用的计数器记录了该对象内存空间当前被变量引用的次数。

对象类别

  • 不可变对象:一旦创建就不可修改的对象,包括字符串、元组、数值类型。该对象所指向的内存中的值不能被改变,当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
  • 可变对象:可以修改的对象,包括列表、字典、集合。该对象所指向的内存中的值可以被改变,变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的地址,通俗点说就是原地改变。

对象的垃圾回收

每个对象保持了一个引用计数器,计数器记录了当前指向该对象的引用的数目,一旦这个计数器为0,这个对象的内存空间就会被自动回收。
比如:

a = 'python'
del a

此时,当把a变量删除时,其实本质只是删除了a变量名,但由于a引用的 ‘python’ 对象,因为a被删除,‘python’ 对象被引用次数变为0,也就自动被Python回收,最终表现就是del a时,‘python’ 也被删除了。

引用

所谓变量对对象的引用,本质是自动创建了变量指向对象内存空间的指针。当将某个对象赋值给某个变量时,可以认为是创建了变量对该对象的引用。

引用方式

简单引用

一个对象 3 (int整数型,不可变对象),用变量名 a 表示:

a = 3

创建一个对象 3 ;同时创建一个变量 a ,并将变量 a 与新的对象 3 相连接。这里需要明确 a 不是对象本身, a 只是对象 3 的引用。

共享引用

因为引用是一种关系,即变量和对象之间的关系,也是指针指向某一块内存空间的关系。既然是变量和对象之间的关系,那么就意味着对于一个对象来说,和不同的变量可以存在多个“引用”关系。

a = 3
b = a

见上例,a 指向对象3,b = a 赋值引用,于是 b 也指向3。此时a 和 b 都共同引用了同一个对象 3。

这里有一个疑问,既然多个变量可以引用同一个对象,那么如果其中一个引用改变了值,会影响到其他指向这个对象的变量么?

输入:

a = 100
b = a
print(a)
print(b)

b = 300
print(a)
print(b)

输出:

100
100
100
300

从上述例子可以看出,对于多个变量引用同一个对象,如果其中一个引用改变了值,不会影响其他指向这个对象的变量。变量值的改变,就相当于指针变量内存空间内值的改变,即指向的对象换了一个,而之前所指向对象的内容并没有受到影响。

这里也有一个特例:列表。指向列表的变量通过索引访问的方式,可以直接改动列表在该索引处存储的值,即列表支持在原处修改。如果此时有两个变量同时引用同一个列表对象,那么其中任何一个变量利用这种方式对列表的修改,都会影响到另外一个变量。为了避免这种情况,往往采取方式是对列表单独拷贝一份。这样两个变量就互不干涉。

赋值

赋值只是复制了新对象的引用,不会开辟新的内存空间。因为不会产生一个独立的对象单独存在,只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。

输入:

print("===== 对于不可变对象赋值 =====")
x = ('a','b','c')
y = x
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象赋值 =====")
x = ['a','b','c']
y = x
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象赋值(外层改变元素) =====")
x = ['a','b','c',['d','e']]
y = x
x.append('f')
print(x)
print(y)

print("===== 对于可变对象赋值(内层改变元素) =====")
x = ['a','b','c',['d','e']]
y = x
x[3].append('f') 
print(x) 
print(y)

输出:

===== 对于不可变对象赋值 =====
('a', 'b', 'c')
('a', 'b', 'c')
139991329637864
139991329637864
===== 对于可变对象赋值 =====
['a', 'b', 'c']
['a', 'b', 'c']
139991320804552
139991320804552
===== 对于可变对象赋值(外层改变元素) =====
['a', 'b', 'c', ['d', 'e'], 'f']   #x外层添加一个元素'f'
['a', 'b', 'c', ['d', 'e'], 'f']   #y跟着添加一个元素'f'
===== 对于可变对象赋值(内层改变元素) =====
['a', 'b', 'c', ['d', 'e', 'f']]   #x[3],即x中子列表添加一个元素'f'
['a', 'b', 'c', ['d', 'e', 'f']]   #y跟着在子列表中添加一个元素'f'

总结

  • 对不可变对象赋值:复制前后值相等,地址相等。重新赋值,也只是新创建一个对象,替换掉旧的而已。
  • 对可变对象赋值:不管是否改变元素,赋值对象都随着改变一起变化。

拷贝

拷贝分浅拷贝和深拷贝

浅拷贝

浅拷贝创建新对象,其内容是原对象的引用。有三种形式: 切片操作,工厂函数,copy模块中的copy函数。
对于列表 lst = [‘a’,‘b’,‘c’,[‘d’,‘e’]]

切片操作

lst1 = lst[:]  #方式1
lst1 = [i for i in lst]   #方式2

工厂函数

lst1 = list(lst)

copy模块

lst1 = copy.copy(lst)

之所以称为浅拷贝,是因为它只拷贝了一层,拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用,在列表lst中有一个嵌套的列表[‘d’,‘e’]。浅拷贝要分两种情况进行讨论:

  1. 当浅拷贝的值是不可变对象(字符串、元组、数值类型)时和“赋值”的情况一样,浅拷贝后对象的id值(id()函数用于获取对象的内存地址)与浅拷贝原来的值相同。
  2. 当浅拷贝的值是可变对象(列表、字典、集合)时会产生一个“不是那么独立的对象”存在。这时也有两种情况:
    – 情况1:拷贝的对象中无复杂子对象,原先值的改变并不会影响浅拷贝的值,同时浅拷贝的值改变也并不会影响原先的值。原先值的id值与浅拷贝后的值的id值不同。
    – 情况2:拷贝的对象中有复杂子对象(例如列表中的一个子元素是一个列表),如果不改变其中复杂子对象,浅拷贝的值改变并不会影响原先的值。 但是如果改变原先的值中的复杂子对象的值则会影响浅拷贝的值,反过来如果改变浅拷贝的值中复杂子对象,则也会影响原先值。

输入:

print("===== 对于不可变对象浅拷贝 =====")
x = ('a','b','c')
y = copy.copy(x)
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象浅拷贝 =====")
x = ['a','b','c']
y = copy.copy(x)
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象浅拷贝(外层改变元素) =====")
x = ['a','b','c',['d','e']]
y = copy.copy(x)
x.append('f')
print(x)
print(y)

print("===== 对于可变对象浅拷贝(内层改变元素) =====")
x = ['a','b','c',['d','e']]
y = copy.copy(x)
x[3].append('f') 
print(x) 
print(y)

输出:

===== 对于不可变对象浅拷贝 =====
('a', 'b', 'c')
('a', 'b', 'c')
139991329638008
139991329638008
===== 对于可变对象浅拷贝 =====
['a', 'b', 'c']
['a', 'b', 'c']
139991320804168
139991354947784
===== 对于可变对象浅拷贝(外层改变元素) =====
['a', 'b', 'c', ['d', 'e'], 'f']   #x外层添加一个元素'f'
['a', 'b', 'c', ['d', 'e']]   #y保持不变
===== 对于可变对象浅拷贝(内层改变元素) =====
['a', 'b', 'c', ['d', 'e', 'f']]   #x[3],即x中子列表添加一个元素'f'
['a', 'b', 'c', ['d', 'e', 'f']]   #y跟着在子列表中添加一个元素'f'

总结

  • 对不可变对象浅拷贝:值相等,地址相等。
  • 对可变对象浅拷贝:值相等,地址不相等。
  • 对可变对象浅拷贝后改变原值:外层改变元素浅拷贝不会随原值变化而变化;内层改变元素,浅拷贝会随着变化。

深拷贝

和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。深拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关联。所以改变原值不会对已经拷贝出来的新对象产生任何影响。深拷贝只有一种形式,即copy模块中的deepcopy函数。

输入:

print("===== 对于不可变对象深拷贝 =====")
x = ('a','b','c')
y = copy.deepcopy(x)
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象深拷贝 =====")
x = ['a','b','c']
y = copy.deepcopy(x)
print(x)
print(y)
print(id(x))
print(id(y))

print("===== 对于可变对象深拷贝(外层改变元素) =====")
x = ['a','b','c',['d','e']]
y = copy.deepcopy(x)
x.append('f')
print(x)
print(y)

print("===== 对于可变对象深拷贝(内层改变元素) =====")
x = ['a','b','c',['d','e']]
y = copy.deepcopy(x)
x[3].append('f') 
print(x) 
print(y)

输出:

===== 对于不可变对象深拷贝 =====
('a', 'b', 'c')
('a', 'b', 'c')
139991329638008
139991329638008
===== 对于可变对象深拷贝 =====
['a', 'b', 'c']
['a', 'b', 'c']
139991354947784
139991329447304
===== 对于可变对象深拷贝(外层改变元素) =====
['a', 'b', 'c', ['d', 'e'], 'f']   #x外层添加一个元素'f'
['a', 'b', 'c', ['d', 'e']]   #y保持不变
===== 对于可变对象深拷贝(内层改变元素) =====
['a', 'b', 'c', ['d', 'e', 'f']]   #x[3],即x中子列表中添加一个元素'f'
['a', 'b', 'c', ['d', 'e']]   #y保持不变

总结

  • 对不可变对象深拷贝:值相等,地址相等。
  • 对可变对象深拷贝:值相等,地址不相等。
  • 对可变对象深拷贝后改变原值:无论原值如何变化,深拷贝都保持不变。

参考:
https://zhuanlan.zhihu.com/p/54011712