
正文
关于 Python 对象拷贝的那点事?
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
概述
在本篇文章中,会先介绍 Python 中对象的基础概念,之后会提到对象的深浅拷贝以及区别。在阅读后,应该掌握如下的内容:
-
理解变量、引用和对象的关系
-
理解 Python 对象中 identity,type 和 value 的概念
-
什么是 mutable 和 immutable 对象?以及它们和 hashable 的关系
-
深浅拷贝的过程以及区别
变量,引用和对象
变量无类型,它的作用仅仅在某个时候引用了特定的对象而已,具体在内存中就是一个指针,仅仅拥有指向对象的空间大小。
变量和对象的关系在于引用,变量引用对象后,也就对应了赋值的过程。
在 python 中一切皆为对象,具体在内存中表示一块内存空间,每一个对象都会具有 identity,type 和 value 这三个内容。
Identity
, 一旦对象被创建后,Identity 的值便不会发生改变。在 Cpython 中,其值体现为内存中保存对象的地址。
is
操作符,比较对象是否相等就是通过这个值。通过
id()
函数查看它的整数形式。
Type
, 和 Identity 一样,在对象创建后,Type 也不会发生变化。它主要定义了一些可能支持的值和操作(如对列表来说,会有求长度的操作)。通过
type()
函数可以得到对象的类型。
Value ,用于表示的某些对象的值。当对象在创建后值可以改变称为 mutable ,否则的话被称为 immutable .
举个例子,比如在 C 中,
int x = 4
在内存中,是先分配了一个
int
类型的内存空间,然后把 4 放进空间内。
而 Python 中,
x = 4
正好相反,是为 4 分配了一块的内存空间,然后用
x
指向它。由于变量可以指向各种类型的对象,因此不需要像 C 一样声明变量。这也就是 Python 被称为动态类型的意义。
并且在 Python 中,变量可以删除,但 对象是无法删除的 。
immutable 和 mutable 对象
immutable 对象拥有一个固定的值,包括 numbers , strings , tuples . 一个新的值被保存时,一个新的对象就会被创建 。这些对象在作为常量的 hash 值中有着非常重要的作用,如作为字典的 key 时。
mutable
对象可以改变自身的值,但
id()
并不会发生改变。
当一些对象包含对其他对象的一些引用时,我们称这些对象为 containers , 例如 list , tuple , dictionary 这些都是 containers . 这里需要注意的是, 一个 immutable containers 可以包含对 mutable 对象的引用(如在 tuple 中包含一个 list)。 但这个对象仍然称为 immutable 对象,因为 Identity 是不变的。
hashable 对象
当一个对象在生命周期内(实现了
__hash__()
方法)hash 值不会发生改变,并可以与其他对象进行比较(实现了
__eq__()
方法),称之为
hashable
对象。
在 Python 内置的 immutable 对象 大多数都是 hashable 对象。 immutable containers (tuples, frozenset)在引用的对象都是 hashable 对象时,才是 hashable 对象。 mutable containers 容器都不是 hashable 对象。用户自定义的类都是 hashable 对象,
浅拷贝与深拷贝
在介绍对象的拷贝前,先介绍一下 Python 中的赋值操作,可以让我们更好的了解拷贝的过程。
赋值操作
赋值操作的右边是简单表达式:
def normal_operation():
# immutable objects
# int
a = 10
b = 10
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = '123'
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:1615046978224 , id of b: 1615046978224
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
tuple_b = (1, 2, 3)
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:1615047009696 , id of b: 1615047024856
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # False
# mutable
# set
set_a = {1, 2, 3}
set_b = {1, 2, 3}
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:1615045625000 , id of b: 1615047012872
print(set_a == set_b) # True
print(set_a is set_b) # False
# list
list_a = [1, 2, 3]
list_b = [1, 2, 3]
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:1615047017800 , id of b: 1615045537352
print(list_a == list_b) # True
print(list_a is list_b) # False
# dict
dict_a = {"name": "xxx", "age": "123"}
dict_b = {"name": "xxx", "age": "123"}
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:1615045521696 , id of b: 1615045522128
print(dict_a == dict_b) # True
print(dict_a is dict_b) # False
在 Cpython 中,
id()
反映了对象在内存中的地址。可以看到,对于
immutable
对象中的
number
和
string
来说,CPython 本身对其做了一定的优化,在创建相同的内容时,使其
指向了相同的内存地址,从而被复用。
但是,Python 不会对所有 mutable 对象执行此操作,因为实现此功能需要一定的运行时成本。对于在内存中的对象来说,必须首先在内存中搜索对象(搜索意味着时间)。对于 number 和 string 来说,搜索到它们很容易,所以才对其做了这样的优化。
对于其他类型的对象,虽然创建的内容相同,但都在内存中完全创建了一块新的区域。
赋值操作的右边是 Python 中已存在的变量:
def assignment_operation():
# immutable objects
# int
a = 10
b = a
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = str_a
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:2676110142128 , id of b: 2676110142128
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
tuple_b = tuple_a
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:2676110191640 , id of b: 2676110191640
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # True
# mutable
# set
set_a = {1, 2, 3}
set_b = set_a
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:2676108788904 , id of b: 2676108788904
print(set_a == set_b) # True
print(set_a is set_b) # True
# list
list_a = [1, 2, 3]
list_b = list_a
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2676110181704 , id of b: 2676110181704
print(list_a == list_b) # True
print(list_a is list_b) # True
# dict
dict_a = {"name": "xxx", "age": "123"}
dict_b = dict_a
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:2676079063328 , id of b: 2676079063328
print(dict_a == dict_b) # True
print(dict_a is dict_b) # True
而当赋值操作的右边是已经存在的 Python 对象时, 不论是什么类型的对象,都没有在内存中创建新的内容,仅仅是声明了一个新的变量指向之前内存中已经创建的对象,就像提供了一个别名一样。
改变赋值后的对象:
def assignment_operation_change():
# immutable objects
# int
a = 10
print("id of a:{}".format(id(a)))
# id of a:1994633728
b = a
a = a + 10
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1994634048 , id of b: 1994633728
print(a == b) # False
print(a is b) # False
# mutable objects
# list
list_a = [1, 2, 3]
list_b = list_a
list_a.append(4)
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2676110181704 , id of b: 2676110181704
print(list_a == list_b) # True
print(list_a is list_b) # True
当修改
imutable
对象时,
由于其本身不可改变,只能在内存中新申请一块新的空间,用于存储修改后的内容
。对应上面
a=20
的操作,这时再判断 a 和 b 时,由于指向了内存的不同位置,所以 a,b不在相等。a 原来指向的内存区域不会被回收,因为现在由 b 指向。可以看到 b 指向的内存地址和 a 之前的指向的内存地址是一致的。
当修改 mutable 对象时,由于都指向相同的内存地址,所以对变量 list_a 修改的操作,也会映射到变量 list_b。
总结一下:
-
指向 imutable 的不同变量,当其中一个变量被修改时,其他变量不受影响,因为被修改后的变量会指向一个新创建的对象。
-
指向 mutable 对象的不同变量,当其中一个变量修改这个对象时,会影响到指向这个对象的所有变量。
浅拷贝
浅拷贝创建了一个对象,这个对象包含了对被拷贝元素的参考。
所以当使用浅拷贝来复制
conainters
对象时,仅仅拷贝了那些嵌套元素的引用。
def shallow_copy():
# immutable objects
# int
a = 10
b = copy(a)
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = copy(str_a)
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:2676110142128 , id of b: 2676110142128
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
# Three methods of shallow copy
# tuple_b = tuple_a[:]
# tuple_b = tuple(tuple_a)
tuple_b = copy(tuple_a)
print(id(tuple_b))
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:2676110191640 , id of b: 2676110191640
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # True
# mutable
# set
set_a = {1, 2, 3}
# Two methods of shallow copy
# set_b = set(set_a)
set_b = copy(set_a)
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:2099885540520 , id of b: 2099888490984
print(set_a == set_b) # True
print(set_a is set_b) # False
# list
list_a = [1, 2, 3]
# Three methods of shallow copy
# list_b = list_a[:]
# list_b = list(list_b)
list_b = copy(list_a)
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2099888478280 , id of b: 2099888478472
print(list_a == list_b) # True
print(list_a is list_b) # False
# dict
dict_a = {"name": "xxx", "age": "123"}
# Two methods of shallow copy
# dict_b = dict(dict_a)
dict_b = copy(dict_a)
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:2099855880480 , id of b: 2099886881024
print(dict_a == dict_b) # True
print(dict_a is dict_b) # False
这里有一点需要注意,对于 string 和 number 来说,正如上面提到的 Cpython 做了相应的优化,让不同的变量指向了相同的内存地址,进而 id 的值是相等的。
但对于元组这个 immutable 元素来说,执行 浅拷贝时,也不会创建一个内存区域,只是返回一个老元组的引用。
对于其他的 mutable 对象,在浅拷贝后都会创建一个新的内存区域,包含了被拷贝元素的引用。
浅拷贝正如它的名字那样,当拷贝嵌套的 mutable 元素时,就会出现问题:
def shallow_copy_change_value():
# list
list_a = [1, 2, 3, [4, 5, 6]]
list_b = copy(list_a)
list_a[0] = 10
list_a[3].append(7)
print('----- list')
print("ia:{} ,b: {}".format(list_a, list_b))
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# a:[10, 2, 3, [4, 5, 6, 7]] ,b: [1, 2, 3, [4, 5, 6, 7]]
# id of a:1698595158472 , id of b: 1698595159752
print(list_a == list_b) # False
print(list_a is list_b) # False
下面是对上面 list 浅拷贝的图解:
执行浅拷贝操作:
在
list_b
执行浅拷贝后,创建一个新的对象,新对象中的
list_a[0]
指向 1.
修改
list_a
操作:
当执行
list_a[0] = 10
操作时,由于
list_a[0]
本身是
number
类型,会重新创建一块区域,用于保存新的值 10. 而新创建的
list_b[0]
并不会受到影响,还会指向之前的内存区域。
当修改
list_a[3]
操作时,由于
list_a[3]
在浅拷贝后,新创建的对象中不会
嵌套创建
一个新的
list_a[3]
对象,仅仅是指向了之前的
list_a[3]
对象。所以当修改
list_a[3]
时,
list_b[3]
也会收到影响。
深拷贝
对于深拷贝操作来说,除了会创建一个新的对象外,会还递归的遍历老对象的中的嵌套元素,并形成新的副本。
def shallow_deepcopy_change_value():
# list
list_a = [1, 2, 3, [4, 5, 6]]
list_b = deepcopy(list_a)
list_a[0] = 10
list_a[3].append(7)
print('----- list')
print("a:{} ,b: {}".format(list_a, list_b))
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2099888478280 , id of b: 2099888478472
print(list_a == list_b) # False
print(list_a is list_b) # False
下面是对应图解过程:
执行深拷贝操作:
修改
list_a
操作:
这里
list_a
和
list_b
已经是完全的不同的两个对象。
总结
在这篇文章中,主要介绍了 Python 中对象,以及对象的拷贝过程,主要有下面几个重要的内容:
-
Python 中变量没有类型,仅仅可看做一个指针,通过引用指向对象。变量可以删除,但对象不行。
-
Python 对象被创建后,会拥有 identity,type 和 value 三个属性。
-
immutable 和 mutable ,主要在于 value 在其生命周期内是否能发生变化。
-
修改 mutable 对象时,所有指向它的变量都会受到影响。修改 immutable 对象时,指向它的其他变量没有影响。
-
immutable 的大多数对象都是 hashable ,但要考虑 immutable containers 的特殊情况。
-
浅拷贝会创建一个新的内存区域(对象),但其内部是对原对象内部引用的拷贝,在使用 mutable 对象时,存在一定的风险。
-
深拷贝不但会创建一个新的内存区域(对象),还会递归的创建原对象的所有嵌套对象,但也带来了一些效率的问题。
参考
Assignment statements in Python are more interesting than you might think
mutable-vs-immutable-objects
term-immutable
03-semantics-variables







