day06深浅拷贝
[TOC]
今日内容概要
- 小数据池(驻留机制)
- 深浅拷贝
- 集合
昨日内容回顾
字典
- 字典是Python中的数据类型之一
- 字典无序,可变(不可哈希),可迭代
- 字典用于存储大量数据,查找方便,查找速度快
- 字典是一种键值对的数据类型:
{'key1': 1, 'key2': 2, 'key3': 3}
- 字典的键要求是可哈希(不可变)数据类型,且唯一
- 字典的值任意
- 字典可以实现数据与数据进行关联:{1: 2, 2: 3, 3: 4, 4: 5}。这是一种编程思想,以后或许会用到
- 字典的增加:
字典['键'] = '值'
,'键'
不存在则增加,'键'
存在则覆盖字典.setdefault('键', '值')
,'键'
存在则返回'键'
对应的'值'
,'键'
不存在,返回新'值'
,新'值'
默认为'None'
。
- 字典的删除:
字典.pop('键')
字典.clear()
,清空字典.popitem()
,随机删除,Python3.6及以上版本默认删除最后一个del 字典['键']
- 字典的修改
字典['键'] = '值'
旧字典.update(新字典)
,新字典中的内容添加到就字典中,新字典将覆盖与旧字典具有相同'键'
的键值对
- 字典的查找
字典['键']
,查找不到会报错字典.get('键')
,查找不到返回None,可指定返回值字典.setdefault('键')
字典.keys()
,查找字典中所有的'键',返回一个高仿列表字典.values()
字典.items()
解构,解包
a = 10 b = 20 a, b = b, a a, b, *c = [1, 2, 3, 4, 5, 6] # *c 聚合 打包 print(a, b, c) print(a, b, *c) # *c 拆散 解包
字典的嵌套
- 通过
'键'
,一层一层地去查找 - 查找的时候要耐心,不能心急
- 通过
今日内容详细
Python中的驻留机制
Python中的驻留机制主要有两个:小数据池和代码块。驻留机制并不是学习Python过程中特别重要的概念,我们学习小数据池的目的主要有两个方面:
- 解决日后写代码过程中可能出现的疑惑。在后期开发中,能
明确
知道有些代码为什么不能正常使用 - 找工作面试时或多或少可能被问到,关键时刻起到装X的作用
先补充一个关键字is
。is(是)
和==(等于)
的作用十分相似,我们在这里将它们进行一个对比:
== :
官方:判断等号两边的内容是否相同
白话:判断两个人的长相是不是一样
is :
官方:判断两边的内容是不是同一个
白话:判断这两个人是不是同一个人
is
是通过内存地址
进行判断,如果内存地址
相同,返回True
;内存地址不同,返回False
。
有了上面的补充,我们来看下面的这个例子:
a = 1000
b = 1000
print(a == b)
print(id(a), id(b))
print(a is b) # 判断a是不是b
在PyCharm中的运行结果是:
True
2565251570544 2565251570544
True
在终端中的运行结果是:
True
1407133449136 1407133448880
False
在PyCharm中运行时我们发现,虽然a和b分别赋值1000,它们在内存中的地址居然是相同的。而在终端中运行同样的代码,却又得到了a与b不相同的结果。
这是Python中的驻留机制造成的影响。Python中,为了节省内存,会将一些小的数据驻留,当再次有变量获得相同的赋值时,不会开辟新的内存空间,而是指向原有的值所处的内存空间。
在PyCharm中运行的是一个py文件,是一个代码块。在代码块中,只要是大于-5的数字都共用同一个内存地址。而在终端中每一行分别为一个代码块,两个赋值操作共用一个小数据池。在小数据池中,只有在-5 ~ 256范围的数字才会被驻留。
小数据池和代码块的缓存机制是这样的:
终端中测试的是小数据池的缓存机制:
数字:-5 ~ 256
字符串:
定义时内容不能为中文和特殊符号,长度不限,只要内容相同,就会进行驻留
Python 3.6解释器字符串进行乘法时,也不能有中文字符和特殊符号,总长度不能超过20
Python 3.7解释器字符串进行乘法时,不能有中文字符和特殊符号,总长度不能超过4096
PyCharm中测试的是代码块的缓存机制:
数字:-5 ~ 正无穷
字符串:
定义内容、长度均不限,只要内容相同,就会驻留
字符串进行乘法时,不能有中文字符和特殊符号,总长度不能超过20
对于字符串总长度的驻留情况,可以这样检验:
a = 'alex' * 6
b = 'alex' * 6
print(a is b)
深浅拷贝
同驻留机制一样,深浅拷贝在编程中的应用其实也不是很多。但是我们依然需要了解。一方面是为了避免后期开发代码时出现问题不知道原因,另一方面也是因为面试时机会都会问到深浅拷贝的问题。
深浅拷贝主要分为三个方面:
- 什么是赋值
- 什么是浅拷贝
- 什么是深拷贝
赋值
变量间赋值操作就是将多个变量的内存地址指向同一个数据的内存地址。例如,对于下面的代码:
a = 10
b = a
print(id(a), id(b))
输出的结果为:1947042192 1947042192
a和b的内存地址相同。这段赋值操作在内存中的程序是:首先,在内存中开辟一块内存空间储存整数10,它的内存地址为1947042192;然后,变量名a指向这个内存空间的内存地址;最后,变量名b同样指向这个内存地址。
对于整型数据这样的不可变数据类型来说,此时,如果重新给a进行赋值,比如赋值为11。将会新开辟一块内存空间来存储整型数据11,然后a将指向11的内存地址(比如:1947042224),原来的指向将不再存在。而b仍然指向10的内存地址1947042192:
用代码来验证就是:
a = 10
b = a
a = 11
print(id(a), id(b))
输出的结果为:1947042224 1947042192
对于可变数据类型的赋值操作则会有些例外。赋值操作的原则都是相同的,新变量名会指向已经存在的数据,而不是新开辟一块内存空间:
lst = [1, 2, 3]
lst1 = lst
print(id(lst), id(lst1))
for i in lst:
print(id(i))
输出的结果为:
1334279969672 1334279969672
1947041904
1947041936
1947041968
首先1、2和3分别开辟各自的内存空间。需要注意的是,列表中存储的是每个元素的内存地址,而不是元素本身。当整型数据内存空间开辟好了之后,又开辟了一块列表空间,其中存储的数据是三个整型数字的内存地址。然后,变量lst指向列表的内存地址1334279969672。最后,赋值操作令变量lst1也指向该列表的内存地址1334279969672。
这些操作看起来繁琐许多,但实际上的原理还是赋值操作只是将不同的变量指向同一个数据的内存地址。
但是现在,如果我们对lst1进行修改,比如增加一个新的元素4,会发生什么呢?
lst = [1, 2, 3]
print(id(lst))
lst1 = lst
lst1.append(4)
print(lst, lst1)
print(id(lst), id(lst1))
for i in lst:
print(id(i))
输出的结果为:
3009179196296
[1, 2, 3, 4] [1, 2, 3, 4]
3009179196296 3009179196296
1947041904
1947041936
1947041968
1947042000
我们发现发生了一件奇怪的事情:虽然我们只对lst1进行了修改,但是lst也同样发生了改变。
这是因为列表是可变数据类型,当我们对列表进行操作时,不会改变列表的内存地址1。使用append方法会在列表存储的内容中添加一个整型数据4的内存空间,但并不会改变变量lst和lst1指向列表内存的状态。故而,虽然只对lst1进行了修改,但是查看lst时,变化是相同的。
举一个形象一点的例子就是,列表就好比是一个书包。最开始,这个书包归小明所有。后来,小明跟好朋友小芳共用了这个书包。小芳有一天在书包里装了一本书,虽然小明没有对这个书包做任何的操作,他依然能够看到自己的书包里多出了一本书来。
对于字典等其他可变数据类型,道理都是相同的。对于可变数据类型的嵌套,也是一样:
dic = {'key1': 2, 'key2': [1, 2, 3]}
dic1 = dic
dic1['key2'].append(4)
print(dic, dic1)
输出的内容为:{'key1': 2, 'key2': [1, 2, 3, 4]} {'key1': 2, 'key2': [1, 2, 3, 4]}
总结起来就是:赋值就是让多个变量指向同一个内存地址,如果这个内存地址的数据是不可变数据类型,修改时会开辟新的内存空间(字符串,数字,布尔值,元组);如果时可变的数据类型,会在原地址进行修改(列表,字典)。
浅拷贝
与赋值略有差异的是,浅拷贝会将数据的外壳新开辟一块内存空间,在新的内存空间中存储的是最外层元素的内存地址。
列表有一个.copy()
方法,用来进行浅拷贝操作:
lst = [1, 2, [3, 4]]
lst1 = lst.copy() #拷贝 or 复制
print(lst, lst1)
print(id(lst), id(lst1))
print(id(lst[0]), id(lst1[0]))
print(id(lst[1]), id(lst1[1]))
print(id(lst[2]), id(lst1[2]))
print(id(lst[2][0]), id(lst1[2][0]))
print(id(lst[2][1]), id(lst1[2][1]))
输出的结果为:
[1, 2, [3, 4]] [1, 2, [3, 4]]
2718491515720 2718491515912
1945534576 1945534576
1945534608 1945534608
2718491515784 2718491515784
1945534640 1945534640
1945534672 1945534672
我们看到,除了最外层的壳子,也就是列表本身被新开辟了一块内存空间之外,无论列表中的元素是否可变,都只是复制了原来列表中元素的内存地址。
最开始,列表lst开辟了一块地址为2718491515720的内存空间,其中,数字1被存储在地址1945534576,数字2被存储在地址1945534608,列表被存储在地址2718491515784。列表中的数字3被存储在地址1945534640,列表中的数字4被存储在地址1945534672。当进行浅拷贝之后,为lst1新开辟一块内存地址为2718491515912的内存空间,其中存储的元素的内存地址跟lst中的元素相同。
此时,如果我们对lst1进行增加操作,比如增加一个元素5:
lst1.append(5)
print(lst, lst1)
输出的结果为:[1, 2, [3, 4]] [1, 2, [3, 4], 5]
只有lst1中增加了元素,lst并没有发生变化。这是因为lst和lst1的内存地址不同,对lst1进行操作并不会影响到lst。
不过如果我们对列表中嵌套的列表进行增加操作,比如:
lst = [1, 2, [3, 4]]
lst1 = lst.copy()
lst1[-1].append(5)
print(lst, lst1)
输出的结果为:[1, 2, [3, 4, 5]] [1, 2, [3, 4, 5]]
不管是lst还是lst1,它们的最后一个元素都是指向的列表[3, 4]的内存地址2718491515784。当对这个列表进行修改时,尽管是使用lst进行的操作,lst1调用列表时,仍然能发现列表的变化。
除了使用.copy()
方法,我们还可以通过切片来实现浅拷贝:
lst = [2, 3, [4, 5], 6]
lst1 = lst
lst2 = lst[:]
lst1[2].append(6)
print(lst, lst1, lst2)
输出的结果为:[2, 3, [4, 5, 6], 6] [2, 3, [4, 5, 6], 6] [2, 3, [4, 5, 6], 6]
深拷贝
深拷贝之后的数据使用同我们主观认为的基本一致:对一个变量的修改不会影响到另一个变量的值。经过深拷贝后的两个变量虽然值相同,但是不受彼此影响。
当然,这也不意味着深拷贝后的两个变量中的每个元素都是不同的。为了节省内存,经过深拷贝后的不可变元素仍然被两个变量共用。因为对不可变元素的改变一定会造成内存地址的变化,所以不需要单独开辟内存空间。但是对于可变元素,不管嵌套多少层,都会开辟新的内存空间。
调用深拷贝方法需要导入一个copy
模块:
import copy
lst = [1000, 2, [3, 4]]
lst1 = copy.deepcopy(lst)
print(id(lst), id(lst1))
print(id(lst[0]), id(lst1[0]))
print(id(lst[1]), id(lst1[1]))
print(id(lst[2]), id(lst1[2]))
print(id(lst[2][0]), id(lst1[2][0]))
print(id(lst[2][1]), id(lst1[2][1]))
返回的结果为:
2277137687880 2277137689160
2277135326064 2277135326064
1945534608 1945534608
2277137687688 2277137689096
1945534640 1945534640
1945534672 1945534672
与我们前面谈到的一样,深拷贝后,两个变量会共用不可变数据,而可变数据类型则会开辟新空间。
深浅拷贝总结
赋值:多个变量名指向同一个内存地址
浅拷贝:只对最外层的壳子开辟内存空间,拷贝第一层元素的内存地址。(列表中存储的是数据的内存地址,我们能看到数据是因为程序通过内存地址找到值,然后显示了出来)
深拷贝:不可变数据共用,可变数据类型不管嵌套多少层都会开辟新空间
集合
集合概览
集合也是Python中的数据类型之一。
集合最重要的一个特点是天然去重。
集合的关键字是set。
空集合的表示方法只有一种:set()
。
集合是一种无序的,可变的,可迭代的,元素唯一且可哈希的数据类型。
集合可以被看作是一种没有值的字典:
- 都用
{}
标识 - 集合的元素是不可变的(可哈希)
- 具有唯一性才做到去重
- 无序,可变,可迭代
我们可以这样定义一个集合:
s = {1, 2, 3, 4}
print(type(s))
输出的结果为:<class 'set'>
如果在定义集合时出现了重复元素,集合会自动去重复:
s = {1,2,3,4,1,1,12,33,3,'421',21,12,3}
print(s)
输出的结果为:{1, 2, 3, 4, 33, 12, '421', 21}
如果多次打印上面的内容,会发现集合是无序的(对于纯数字集合这种效果不是很明显。如果里面又多种数据类型,无序性会很明显)。
在Python中,能够存储数据的结构被称作为容器。我们学过的能作为容器的数据类型有:列表、元素和字典。很显然,集合也是一种容器。
集合的增加
.update()
方法可以将输入的参数迭代添加到集合中,参数必须是可迭代数据类型:
s = set()
s.update('alex')
print(s)
输出的结果为:{'e', 'l', 'a', 'x'}
.add()
方法可以将单独的元素直接添加到集合中:
s = set()
s.add('alex')
print(s)
输出的结果为:{'alex'}
结合的删除
.pop()
方法会随机删除集合中的元素,并将删除的元素返回:
s = {"a","b",3,"c"}
print(s.pop(), s)
这个式子多次运行后会发现,每次删除掉的数据都是不同的。正因为这种数据删除方法的随机性和不确定性,十分不建议在编程时使用。
.remove()
方法可以指定元素删除集合中的内容:
s = {"a","b",3,"c"}
s.remove('b')
print(s)
输出的结果为:{'c', 3, 'a'}
.clear()
方法用来将集合清空:
s = {"a","b",3,"c"}
s.clear()
print(s)
输出的结果为:set()
这里需要注意的是,为了防止和空字典的表示方法{}
冲突,空集合只有set()
一种表示方法。
集合的修改
集合没有直接进行修改的方法,不过可以通过下面两种方式间接改变集合的内容:
- 先删后加(或者先加后删)
- 转换为列表数据类型后进行修改
集合的查找
因为集合是无序的,所以不能通过索引查找。
好在集合可以迭代,可以通过for循环进行查看:
s = {"a","b",3,"c"}
for i in s:
print(i)
集合关系
同数学中集合的概念很相似,Python中的集合也可以进行交集、并集、差集、补集、超级和子集等操作:
交集
交集用来找出两个集合中相同的元素,用&
表示:
python = {"海绵","孙一帆","岳新力","大圣"}
linux = {"海绵","大圣","meet","alex"}
print(python & linux)
输出的结果为:{'海绵', '大圣'}
并集
并集是将两个集合合并,并将重复元素去除掉,用|
表示:
python = {"海绵","孙一帆","岳新力","大圣"}
linux = {"海绵","大圣","meet","alex"}
print(python | linux)
输出的结果为:{'岳新力', 'meet', '海绵', '孙一帆', '大圣', 'alex'}
差集
差集用来消除前面集合中与后面集合重复的元素,用-
表示:
python = {"海绵","孙一帆","岳新力","大圣"}
linux = {"海绵","大圣","meet","alex"}
print(python - linux)
print(linux - python)
输出的结果为:
{'岳新力', '孙一帆'}
{'alex', 'meet'}
补集
补集也称作反差集,对称差集,是两个集合中不相同元素组成的集合,用^
表示:
python = {"海绵","孙一帆","岳新力","大圣"}
linux = {"海绵","大圣","meet","alex"}
print(python ^ linux)
输出的结果为:{'岳新力', 'alex', '孙一帆', 'meet'}
超集
超集也称作父集,用来判断后面集合中是否每一个元素都在前一个集合中且两个集合不相同,若都在且两个集合不完全一样,返回True,若不都在或两个集合完全一样,返回False。超集用>
表示:
python = {"海绵","孙一帆","岳新力","大圣"}
linux = {"海绵","大圣","meet","alex"}
l_son = {'meet'}
print(python > linux)
print(python > python)
print(linux > l_son)
输出的结果为:
False
False
True
子集
子集用来判断前面集合中的每一个元素是否都在后一个集合中且两个元素都不相同,若都在且两个集合不完全一样,返回True,若不都在或两个集合完全一样,返回False。超集用<
表示:
在最后,补充一点感想。今天老师的一句话对我很有感触。他说,计算机不会出错,错的都是人。
我觉得计算机不会出错这件事反映出来的不是人笨,而是计算机还不够智能。会随机地出错,是计算机走向智能的一大步。
今天学到的.pop()方法,可以随机地删除数据,也就是制造错误,或许在人工智能的研究中很有用。
-
虽然再次运行内存地址较上次发生了变化,但那是因为每次运行都会重新开辟内存空间。上面的代码在赋值前打印了内存地址,与所有赋值操作后比较,仍然相同,可证明列表的内存地址没有改变。↩