数据模型浅谈
对象的id
在Python中,一切数据皆为对象,对象是Python对数据的一种抽象。每一个对象皆有其identity,type,value。对象一旦创建,其id便不会改变,你可以将其视作对象在内存中的地址。is运算符比较的两个对象的id是否相同,id()函数返回代表id的整数形式。
对象的type
对象的类型决定了该对象所支持的操作,以及定义了该对象可能的值。type()函数可以获取对象的类型。和对象的id一样,对象的类型也是无法改变的。
对象的value
对象的值是可以改变的。可变对象的值是可变的,不可变对象的值是无法改变的。对象的可变性(immutability)是通过其类型来决定的,如数字类型,字符串类型和元组类型都是不可变类型,而字典类型和列表类型则是可变类型。
不可变容器对象包含可变对象时,其值是可变的。但整个容器对象却是不可变的。例如元组对象嵌套列表对象时,列表对象改变时,整个元组对象仍是不可变对象。
赋值机制
-
简单数据类型赋值。
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)))
首先,创建一个字符串对象,变量str1指向\'hello world\'。str2 = str1的作用是使str2也指向\'hello world\'。str1和str2保存相同的内存地址。
之后创建新的字符串对象\'hi world\',并改变str1继而指向新的字符串对象。而由输出结果可知,str2的id并没有发生改变,说明str2仍然指向第一个字符串对象。 -
列表类型赋值。
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)))
首先,创建一个列表对象lst1,并让lst2和lst1都指向同一个列表。后面的append操作和pop操作,不管是针对lst1还是lst2,其实操作的都是同一个列表。两个变量均指向同一个列表,则对任意一个变量操作,都会影响到另一个。
总结,不管是简单或复杂的数据类型,赋值操作均将多个变量指向一个数据块。若对其中一个变量重新赋值,则另一个变量仍指向之前的数据块。对于复杂数据类型来说,简单的追加等操作,并不改变数据块的地址,也不会改变变量的实际指向。因此,对一个变量的操作,实际上是对整个数据块的操作,进而影响到其他变量。
Python中的赋值语句不会复制对象,它们会在目标和对象之间创建绑定。
关于拷贝
需求:有时候我们需要的是完全复制一份新的数据,原有数据的改变不会影响到新的变量所指向的数据。这时,Pyhton引入的拷贝的概念。
对于可变项目或包含可变项目的集合,有时需要副本,以便可以更改一个副本而不更改其他副本。Python为这种需求提供了一个专门的模块copy。包括深浅拷贝,即deepcopy和copy。浅层拷贝和深层拷贝的区别仅存在于复合对象中(对象包含其他对象,例如列表或类实例)。
-
浅拷贝
浅拷贝构造一个新的复合对象,然后(尽可能)将引用插入到原始对象中。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)
由上面的代码输出结果可知,通过copy模块的浅拷贝,对source_lst进行普通数据类型的追加,修改等操作,不会引起copy_lst的改变。但对嵌套列表的修改操作却会同时引起source_lst和copy_lst的改变,原因是浅拷贝只拷贝了嵌套列表的整个引用,并没有重新开辟新的数据空间。
-
深拷贝
深层拷贝构造一个新的复合对象,然后递归地将副本插入到原始对象中找到的对象。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)
仅将copy.copy(source_lst)更改为copy.deepcopy(source_lst),其他都保持不变。对source_lst进行普通数据类型的追加,修改等操作,不会引起deepcopy_lst的改变。对于source_lst的嵌套列表修改,deepcopy_lst也没有发生改变,原因是深层拷贝,采用递归的方式,拷贝所有的数据类型,重新开辟数据空间。