关于 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 对象中的 numberstring 来说,CPython 本身对其做了一定的优化,在创建相同的内容时,使其 指向了相同的内存地址,从而被复用。

但是,Python 不会对所有 mutable 对象执行此操作,因为实现此功能需要一定的运行时成本。对于在内存中的对象来说,必须首先在内存中搜索对象(搜索意味着时间)。对于 numberstring 来说,搜索到它们很容易,所以才对其做了这样的优化。

对于其他类型的对象,虽然创建的内容相同,但都在内存中完全创建了一块新的区域。

赋值操作的右边是 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

这里有一点需要注意,对于 stringnumber 来说,正如上面提到的 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 浅拷贝的图解:

执行浅拷贝操作:

关于 Python 对象拷贝的那点事?

list_b 执行浅拷贝后,创建一个新的对象,新对象中的 list_a[0] 指向 1.

修改 list_a 操作:

关于 Python 对象拷贝的那点事?

当执行 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

下面是对应图解过程:

执行深拷贝操作:

关于 Python 对象拷贝的那点事?

修改 list_a 操作:

关于 Python 对象拷贝的那点事?

这里 list_alist_b 已经是完全的不同的两个对象。

总结

在这篇文章中,主要介绍了 Python 中对象,以及对象的拷贝过程,主要有下面几个重要的内容:

  • Python 中变量没有类型,仅仅可看做一个指针,通过引用指向对象。变量可以删除,但对象不行。

  • Python 对象被创建后,会拥有 identity,type 和 value 三个属性。

  • immutablemutable,主要在于 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

上一篇:移植openssl


下一篇:ZigZag Conversion1