Python赋值与深浅拷贝

时间:2022-11-03 19:51:52

数据模型浅谈

对象的id

在Python中,一切数据皆为对象,对象是Python对数据的一种抽象。每一个对象皆有其identitytypevalue。对象一旦创建,其id便不会改变,你可以将其视作对象在内存中的地址。is运算符比较的两个对象的id是否相同,id()函数返回代表id的整数形式。

对象的type

对象的类型决定了该对象所支持的操作,以及定义了该对象可能的值。type()函数可以获取对象的类型。和对象的id一样,对象的类型也是无法改变的。

对象的value

对象的值是可以改变的。可变对象的值是可变的,不可变对象的值是无法改变的。对象的可变性(immutability)是通过其类型来决定的,如数字类型,字符串类型和元组类型都是不可变类型,而字典类型和列表类型则是可变类型。
不可变容器对象包含可变对象时,其值是可变的。但整个容器对象却是不可变的。例如元组对象嵌套列表对象时,列表对象改变时,整个元组对象仍时不可变对象。

赋值机制

  1. 简单数据类型赋值:

    str1 = 'hello world'
    str2 = str1
    print('str1: ' + str1 + '; id: ' + str(id(str1)))
    print('str2: ' + str2 + '; id: ' + str(id(str2)))
    print('-' * 35)
    str1 = 'hi world'
    print('str1: ' + str1 + '; id: ' + str(id(str1)))
    print('str2: ' + str2 + '; id: ' + str(id(str2)))

    Python赋值与深浅拷贝

    首先,创建一个字符串对象,变量str1指向'hello world'。str2 = str1的作用是使str2也指向'hello world'。str1和str2保存相同的内存地址。
    之后创建新的字符串对象'hi world',并改变str1继而指向新的字符串对象。而由输出结果可知,str2的id并没有发生改变,说明str2仍然指向第一个字符串对象。
  2. 列表类型赋值:

    lst1 = [1, 2, 3, 4, 5]
    lst2 = lst1
    print('lst1: ' + str(lst1) + '; id: ' + str(id(lst1)))
    print('lst2: ' + str(lst2) + '; id: ' + str(id(lst2)))
    print('*' * 40)
    lst1.append(6)
    print('lst1: ' + str(lst1) + '; id: ' + str(id(lst1)))
    print('lst2: ' + str(lst2) + '; id: ' + str(id(lst2)))
    lst2.pop()
    print('lst1: ' + str(lst1) + '; id: ' + str(id(lst1)))
    print('lst2: ' + str(lst2) + '; id: ' + str(id(lst2)))

    Python赋值与深浅拷贝

    首先,创建一个列表对象lst1,并让lst2和lst1都指向同一个列表。后面的append操作和pop操作,不管是针对lst1还是lst2,其实操作的都是同一个列表。两个变量均指向同一个列表,则对任意一个变量操作,都会影响到另一个。

总结,不管是简单或复杂的数据类型,赋值操作均将多个变量指向一个数据块。若对其中一个变量重新赋值,则另一个变量仍指向之前的数据块。对于复杂数据类型来说,简单的追加等操作,并不改变数据块的地址,也不会改变变量的实际指向。因此,对一个变量的操作,实际上是对整个数据块的操作,进而影响到其他变量。
Python中的赋值语句不会复制对象,它们会在目标和对象之间创建绑定。

关于拷贝

需求:有时候我们需要的是完全复制一份新的数据,原有数据的改变不会影响到新的变量所指向的数据。这时,Pyhton引入的拷贝的概念。
对于可变项目或包含可变项目的集合,有时需要副本,以便可以更改一个副本而不更改其他副本。Python为这种需求提供了一个专门的模块copy。包括深浅拷贝,即deepcopy和copy。浅层拷贝和深层拷贝的区别仅存在于复合对象中(对象包含其他对象,例如列表或类实例)。

  1. 浅拷贝
    浅拷贝构造一个新的复合对象,然后(尽可能)将引用插入到原始对象中。

    import copy
    
    source_lst = ['str1', 'str2', 'str3', ['str4', 'str5', 'str6']]
    copy_lst = copy.copy(source_lst)
    print(source_lst)
    print(copy_lst)
    print('*' * 50)
    
    source_lst.append('append str')
    print(source_lst)
    print(copy_lst)
    print('*' * 50)
    
    source_lst[0] = 'modified str1'
    print(source_lst)
    print(copy_lst)
    print('*' * 50)
    
    source_lst[3][0] = 'modified str4'
    print(source_lst)
    print(copy_lst)
    print('*' * 50)

    Python赋值与深浅拷贝

    由上面的代码输出结果可知,通过copy模块的浅拷贝,对source_lst进行普通数据类型的追加,修改等操作,不会引起copy_lst的改变。但对嵌套列表的修改操作却会同时引起source_lst和copy_lst的改变,原因是浅拷贝只拷贝了嵌套列表的整个引用,并没有重新开辟新的数据空间。
  2. 深拷贝
    深层拷贝构造一个新的复合对象,然后递归地将副本插入到原始对象中找到的对象。

    import copy
    
    source_lst = ['str1', 'str2', 'str3', ['str4', 'str5', 'str6']]
    deepcopy_lst = copy.deepcopy(source_lst)
    print(source_lst)
    print(deepcopy_lst)
    print('*' * 50)
    
    source_lst.append('append str')
    print(source_lst)
    print(deepcopy_lst)
    print('*' * 50)
    
    source_lst[0] = 'modified str1'
    print(source_lst)
    print(deepcopy_lst)
    print('*' * 50)
    
    source_lst[3][0] = 'modified str4'
    print(source_lst)
    print(deepcopy_lst)
    print('*' * 50)

    Python赋值与深浅拷贝

    仅将copy.copy(source_lst)更改为copy.deepcopy(source_lst),其他都保持不变。对source_lst进行普通数据类型的追加,修改等操作,不会引起deepcopy_lst的改变。对于source_lst的嵌套列表修改,deepcopy_lst也没有发生改变,原因是深层拷贝,采用递归的方式,拷贝所有的数据类型,重新开辟数据空间。