上一篇文章中我们将字符串常见的一些用法总算介绍完了,至此我们才开始进入本章的正题,施法前摇有点长啊,但因为字符串实在是太重要了,先了解一下它还是很有必要的。
我们先来聊一下Python中的列表,列表是一种有序的、可变的、可索引的异构数据结构,它使用[]来定义,可以存放零个或多个任意类型的Python对象(比如:数字、字符串、布尔值、甚至是其他列表等等),允许存放重复元素。
在Python中,创建一个列表是非常容易的,我们在前面已经说到用[]来定义列表,那么创建一个空的列表就可以这么写:
# 空列表empty_list = []如果在声明列表变量的时候就想放入一些元素,也很简单:
# 单一元素列表num_list = [1, 2, 3]Python的列表是可以存放重复元素和多种类型元素的,我们来看一下例子:
# 存在重复元素列表repeat_list = [1, 1, 2, 2]# 混合多种类型的列表mix_list = [1, 'a', 3.1415926, True, [4, 5]]对于已经存在的列表如何访问列表中的元素呢?
可以通过索引的方式访问,索引是从0开始的,比如对于一个列表elems = [1, 'a', 2, 'b', 3, 'c'],我们想访问第一个元素,就可以这么做:
elems = [1, 'a', 2, 'b', 3, 'c']print(elems[0]) # 输出:1按照这个思路,如果我们想访问最后一个元素c,它在列表中的索引值是5,则可以这样写:
elems = [1, 'a', 2, 'b', 3, 'c']print(elems[5]) # 输出:cPython中的列表是支持反向索引的,也就是从右到左,索引从-1开始,对于访问元素c,我们还可以这样写:
elems = [1, 'a', 2, 'b', 3, 'c']print(elems[-1]) # 输出:c列表是可变的,通过索引我们不仅可以访问列表中的元素,还可以修改列表中的元素,比如我现在想修改elems列表的第0个元素,可以这么做:
elems = [1, 'a', 2, 'b', 3, 'c']elems[0] = 5print(elems) # 输出:[5, 'a', 2, 'b', 3, 'c']通过索引修改列表中的元素,列表中元素的数量是不变的,对于已有的列表想增加元素怎么做?
那这就要看怎么增加元素了,如果我们只是想在列表的末尾追加元素,比如在列表elems = [1, 'a', 2, 'b', 3, 'c']中追加一个元素d,可以用append()方法来实现:
elems = [1, 'a', 2, 'b', 3, 'c']elems.append('d')print(elems) # 输出:[1, 'a', 2, 'b', 3, 'c', 'd']此时如果我们想在字母d前加入一个数字,append()方法显然是不行了,好在Python中提供了一个insert()方法,可以在指定的索引位置处插入一个新的元素,那么在d元素前插入一个数字就可以这么做了:
elems = [1, 'a', 2, 'b', 3, 'c', 'd']elems.insert(-1, 4)print(elems) # 输出:[1, 'a', 2, 'b', 3, 'c', 4, 'd']在这里我使用了反向索引,-1是最后一个位置,新插入的元素会占据最后一个位置,而原先的最后一个元素就会后移,这就如同现实生活中的排队,如果你插队占据了别人的位置,那么这个位置及其后面的人都要往后移一个位置。
append()和insert()方法每次都是在列表中增加一个元素,如果想追加的元素也是列表呢?
比如现在列表elems = [1, 'a', 2, 'b', 3, 'c', 4, 'd'],还有第二个列表elems1 = [5, 'e'],如何将列表elems1加入到elems中呢?
这实际上是列表的合并了,在Python中可以使用extend()方法:
elems = [1, 'a', 2, 'b', 3, 'c', 4, 'd']elems1 = [5, 'e']elems.extend(elems1)print(elems) # 输出:[1, 'a', 2, 'b', 3, 'c', 4, 'd', 5, 'e']合并列表,除了用extend(),我们还可以直接用+运算符来实现:
elems = [1, 'a', 2, 'b', 3, 'c', 4, 'd']elems1 = [5, 'e']res = elems + elems1 # 使用+来合并列表print(res) # 输出:[1, 'a', 2, 'b', 3, 'c', 4, 'd', 5, 'e']extend()方法与使用+运算符合并列表的不同之处在于,extend()方法会修改原有的列表,而+运算符则不修改。
现在有这么一个列表elems = [1, 1, 'a', 2, 2, 'b', 'b'],这里面有一些重复的元素,我想把这些重复的元素删掉,我观察到elems前两个元素就有一个重复的1,所以我想先删除元素1,在Python中可以用remove()方法:
elems = [1, 1, 'a', 2, 2, 'b', 'b']elems.remove(1)print(elems) # 输出:[1, 'a', 2, 2, 'b', 'b']remove()方法只需要指定删除的元素的值即可,它只删除匹配到的第一个元素,不会将指定的所有元素值都删除。
使用remove(1)之后就删除了第一个重复的元素,此时的elems列表还有中间和末尾的重复值,继续使用remove()方法当然是可以的,但Python中还有其他的方法用于删除元素,我也想尝试一下,比如删除最后一个元素b,可以用pop()方法:
elems = [1, 'a', 2, 2, 'b', 'b']elems.pop()print(elems) # 输出:[1, 'a', 2, 2, 'b']pop()方法默认情况下是删除列表的最后一个元素,但它是支持传入索引参数的,也就是我们可以删除指定位置的元素,比如我们指定删除elems中倒数第二个元素b:
elems = [1, 'a', 2, 2, 'b', 'b']elems.pop(-2)print(elems) # 输出:[1, 'a', 2, 2, 'b']目前列表elems = [1, 'a', 2, 2, 'b']中还有重复的元素2,我们可以再换一种方法来尝试,使用del语句:
elems = [1, 'a', 2, 2, 'b']del elems[2]print(elems) # [1, 'a', 2, 'b']del语句可以删除列表中指定位置的元素,如果我们不指定索引呢?
elems = [1, 'a', 2, 2, 'b']del elemsprint(elems)它会删除整个elems列表,并且上面的代码还会出现NameError: name 'elems' is not defined异常提示,因为它不仅仅是删除内容,把列表变量本身也删除了。
如果只是想清空列表,我们可以用clear():
elems = [1, 'a', 2, 2, 'b']elems.clear()print(elems) # 输出:[]当然,因为列表是支持切片的,我们可以通过删除切片来达到清空的效果:
elems = [1, 'a', 2, 2, 'b']del elems[:]print(elems) # 输出:[]这同样意味着我们给定索引范围是可以删除指定范围的元素的:
elems = [1, 'a', 2, 2, 'b']del elems[0:3]print(elems) # 输出:[2, 'b']在列表中判断某个元素是否存在是比较基础和常见的一个需求,我们可以用in或not in关键字来实现。
比如现在有一个列表elems = [1, 1, 'a', 2, 'b'],我现在想判断元素1是否存在,如果存在则删除,元素3是否存在,不存在则追加:
elems = [1, 1, 'a', 2, 'b']if 1 in elems: elems.remove(1) print(elems) # 输出:[1, 'a', 2, 'b']if 3 not in elems: elems.append(3) print(elems) # 输出:[1, 'a', 2, 'b', 3]in或not in判断的结果都是布尔值,也就是True或False。它们不能返回元素的精确位置,如果我们想获取指定元素在列表中的精确位置,则需要使用index()方法。
比如现在有一个列表elems = [1, 'a', 2, 'c'],我想把其中的元素c修改改为b,则可以这样做:
elems = [1, 'a', 2, 'c']if 'c' in elems: idx = elems.index('c') elems[idx] = 'b'print(elems) # 输出:[1, 'a', 2, 'b']index()方法配合in来使用会保险一些,因为如果查找的元素不存在会出现ValueError异常。index()方法还支持指定起始索引,也就是我们可以指定查找范围。
统计其实是查找一种延伸,它不仅要找,而且要计数,列表操作中提供了一个count()方法用于统计元素出现的次数。
比如现在有一个列表elems = [1, 'a', 2, 'b', 3, 3, 3, 'c'],我现在想统计元素3出现了多少次,可以这么做:
elems = [1, 'a', 2, 'b', 3, 3, 3, 'c']print(elems.count(3)) # 输出:3如果还想获取列表有多少个元素,也就是列表的长度,可以用Python内置的len()函数:
elems = [1, 'a', 2, 'b', 3, 3, 3, 'c']print(len(elems)) # 输出:8现在有一个列表nums = [1, 5, 0, 3, 2, 7, 8, 6, 4],我们可以发现它存储的数字是乱序的,想让它输出一个有序的列表,该怎么操作呢?
在Python中列表有一个sort()方法专门用于排序,我们可以这么做:
nums = [1, 5, 0, 3, 2, 7, 8, 6, 4]nums.sort()print(nums) # 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8]默认情况下得到的排序是升序,如果想让nums中的数字进行降序排序,可以使用sort()方法中的reverse参数:
nums = [1, 5, 0, 3, 2, 7, 8, 6, 4]nums.sort(reverse=True)print(nums) # 输出:[8, 7, 6, 5, 4, 3, 2, 1, 0]reverse参数是布尔类型,默认为False,也就是升序。
sort()方法还有一个参数key,key是一个可调用对象,也就是说它可以是一个自定义函数、lambda表达式或内置函数等。
比如现在有一个列表words = ["Python","C", "Java", "JavaScript"],默认情况下调用sort()函数,是这样的:
words = ["Python","C", "Java", "JavaScript"]words.sort()print(words) # 输出: ['C', 'Java', 'JavaScript', 'Python']sort()方法对于字母的排序是按照Unicode编码值排序的,如果我们想自己定义一个规则排序,就可以利用key这个参数,比如我希望words列表按照字母长度进行排序,就可以将key指定为len()函数:
words = ["Python","C", "Java", "JavaScript"]words.sort(key=len)print(words) # 输出: ['C', 'Java', 'Python', 'JavaScript']指定了key=len,Python会遍历列表的每一个元素,每一个元素都会传入len()函数,然后用返回值作为排序的依据。
sort()方法是列表的方法,Python中还有一个sorted()内置函数,用法和列表的sort()方法基本一致,不同之处在于sort()方法是直接修改原列表,而sorted()函数适用于所有可迭代对象,并且会返回一个新的列表作为排序结果。
我们在聊sort()函数排序时,说到它支持reverse参数,可以进行结果的反转,列表中还提供了一个独立的反转方法:reverse()。
比如现在有一个已经排好序的列表nums = [0, 1, 2, 3, 4, 5, 6],我希望能够降序输出,那就可以利用列表的reverse()方法:
nums = [0, 1, 2, 3, 4, 5, 6]nums.reverse()print(nums) # 输出:[6, 5, 4, 3, 2, 1, 0]列表的reverse()方法是没有返回值的,它直接修改原列表。如果我们不想修改原列表,可以使用Python中内置函数reversed():
nums = [0, 1, 2, 3, 4, 5, 6]res = reversed(nums)print(list(res)) # 输出:[6, 5, 4, 3, 2, 1, 0]reversed()函数返回的是迭代器对象,我们需要通过list()转换为列表便于查看输出结果。reversed()函数适合处理大数据列表,节省内存。
还有一种比较简洁的反转列表的方法是利用切片:
nums = [0, 1, 2, 3, 4, 5, 6]print(nums[::-1]) # 输出:[6, 5, 4, 3, 2, 1, 0]在字符串部分我们聊到过切片,列表中的切片也是一样的,我们来回忆一下基本的语法:[start:stop:step],切片中是包含三个参数,start起始索引,end结束索引,step步长,step为负数时就是从右往左取元素,也就实现了列表反转。
我们有时候在处理列表数据时,为了避免修改原始列表,可以先复制一份列表数据,然后对副本进行操作。
先来看一个问题,下面这段代码是复制列表了吗?
nums = [1, 2, 3]copy = nums答案是没有复制,这只是声明了一个新的变量指向列表,修改数据会对原始列表造成影响。那么Python中如何复制列表呢?
我们在前面提到过的切片,算是一种最简洁的列表复制了,因为它会产生一个新的列表,修改数据也不会对原始列表产生影响:
nums = [1, 2, 3]res = nums[:] # 使用切片res.append(4)print(res) # 输出:[1, 2, 3, 4]print(nums) # 输出:[1, 2, 3]Python中列表还提供了一个copy()方法用于复制:
nums = [1, 2, 3]res = nums.copy() # 使用copy()方法res.append(4)print(res) # 输出:[1, 2, 3, 4]print(nums) # 输出:[1, 2, 3]或者也可以使用list()构造函数来实现:
nums = [1, 2, 3]res = list(nums)res.append(4)print(res) # 输出:[1, 2, 3, 4]print(nums) # 输出:[1, 2, 3]上面这几种复制列表的方法都存在一个问题,那就是他们对于嵌套列表的复制会存在问题,我们来看一个例子:
nums = [1, 2, 3, [4, 5]]res = list(nums)res[3].append(6)print(res) # 输出:[1, 2, 3, [4, 5, 6]]print(nums) # 输出:[1, 2, 3, [4, 5, 6]]在nums列表中包含了一个子列表[4, 5],我们复制了一份并对副本中的子列表新增了一个元素,从输出结果看,列表副本和原始列表都被修改了,这显然是存在问题的,这种复制也被称为浅拷贝,Python中通过使用copy模块来解决这个问题,也就是实现深拷贝:
import copynums = [1, 2, 3, [4, 5]]res = copy.deepcopy(nums)res[3].append(6)print(res) # 输出:[1, 2, 3, [4, 5, 6]]print(nums) # 输出:[1, 2, 3, [4, 5]]如果我们想实现从0-10的数字中筛选出所有的偶数,将这些偶数存储在一个列表中,比较直接的想法是可以这么做:
even_nums = []for i in range(0, 11): if i % 2 == 0: even_nums.append(i)print(even_nums) # 输出:[0, 2, 4, 6, 8, 10]我们使用for循环和if判断语句再加上元素处理就完成了偶数的筛选,对于这种循环+条件筛选的结构,在Python中还有更简洁的写法,那就是列表推导式,我们可以这么写:
even_nums = [i for i in range(0, 11) if i % 2 == 0]print(even_nums) # 输出:[0, 2, 4, 6, 8, 10]它的基本语法是:[表达式 for 变量 in 可迭代对象 if 条件]。
对于上面的例子,表达式就是变量i,也是for循环的变量,之所以说这个位置是表达式是因为它的确可以写表达式,比如说对筛选出的偶数都加上1,就可以这么写:
res = [i + 1 for i in range(0, 11) if i % 2 == 0]print(res) # 输出:[1, 3, 5, 7, 9, 11]虽然列表推导式简化了for循环的条件筛选操作,但对于复杂的逻辑还是使用普通的循环即可,因为复杂的逻辑会导致列表推导式难以理解。
元组是一种不可变、有序、可哈希的序列型数据结构,它的元素也是可以存储任意数据类型(数字、字符串、列表、元组),使用()来定义,元素之间使用逗号分隔,小括号可以省略,因为逗号才是元组的核心标识。
元组的操作和列表大体上是一致的,只是由于元组具有不可变性,无法执行修改类的操作,可以将其理解为只读的列表。
常规创建元组是这样的:
t = (1, 'a', True, [2, 3], (3.14, 5.6))当然最外层不用括号包裹也是可以的:
t = 1, 'a', True, [2, 3], (3.14, 5.6)如果只有一个元素,括号可以不写,但必须有逗号,否则就会被识别为基本类型了:
t = (1,) 如果一个元素也没有,也就是创建一个空元组,也很简单:
t = ()元组的访问和列表一样,通过索引读取。比如现在有一个元组t = (1, 'a', 2, 'b'),我想访问第一个元素和最后一个元素的值:
t = (1, 'a', 2, 'b')print(t[0]) # 输出:1print(t[-1]) # 输出:b也可以通过切片读取一个片段:
t = (1, 'a', 2, 'b')print(t[0:2]) # 输出:(1, 'a')和列表一样,判断元素是否存在,可以使用in或not in关键字:
t = (1, 'a', 2, 'b')print('a' in t) # 输出:Trueprint(3 not in t) # 输出:True元组也有index()方法用于获取元素的精确位置:
t = (1, 'a', 2, 'b')print(t.index('a')) # 输出:1当然,如果我们查找的元素不存在,也会报ValueError异常。
元组同样有count()方法来统计元素的数量:
t = (1, 'a', 2, 2, 2, 'b')print(t.count(2)) # 输出:3获取元组的长度,也依然使用len()函数:
t = (1, 'a', 2, 2, 2, 'b')print(len(t)) # 输出:6元组的拼接,也就是合并两个元组,可以像列表一样用+运算符,但不像列表还提供了一个extend()方法:
t1 = (1, 2)t2 = (3, 4)t = t1 + t2print(t) # 输出:(1, 2, 3, 4)还有一种方法是利用itertools模块中的chain()方法,不过它返回的结果需要用tuple()构造方法进行转换:
import itertoolst1 = (1, 2)t2 = (3, 4)t = tuple(itertools.chain(t1, t2))print(t) # 输出:(1, 2, 3, 4)元组中还对乘法运算符(*)进行了重载,可以实现元组的重复:
t1 = (1, 2)t = t1 * 2print(t) # 输出:(1, 2, 1, 2)解包就是将元组中的元素分配给多个变量。
比如我们用一个元组来存储点坐标,想获取对应坐标轴的值:
point = (1, 2)x = point[0]y = point[1]print(f"{x}, {y}")常规的思路就是按照索引来获取不同坐标轴上的值,但是如果使用解包,就会更简洁:
point = (1, 2)x, y = point # 解包print(f"{x}, {y}") # 输出:1, 2这里需要注意的是,对于元组中只有一个元素是,解包时变量后面要有逗号,比如:
t = (1,)x, = t # 需要有逗号print(x) # 输出:1用解包来交换变量是比较经典的一个技巧,传统情况下我们交换两个变量是需要一个临时变量的:
x = 1y = 2temp = xx = yy = tempprint(f"{x}, {y}") # 输出:2, 1使用解包,一行代码即可搞定变量交换,而且不需要临时变量:
x = 1y = 2x, y = y, x # 解包交换,一行搞定print(f"{x}, {y}") # 输出:2, 1在解包中我们还可以用*来接收元组中剩余的所有元素,比如我们现在有这样一个元组t = (1, 2, 3, 4, 5, 6),我想获取首尾两个元素,可以这么做:
t = (1, 2, 3, 4, 5, 6)first, *middle, last = tprint(f"{first}, {middle}, {last}") # 输出:1, [2, 3, 4, 5], 6我发现中间部分目前没什么用,所以其实不接收也可以,于是就可以用_占位符来忽略无关紧要的数据:
t = (1, 2, 3, 4, 5, 6)first, *_, last = tprint(f"{first}, {last}") # 输出:1, 6字典是一种无序的、可变的、以键值对形式存储数据的数据结构,它用{}包裹,键和值时间用:分割,多个键值对之间用逗号,分隔。
需要注意的是字典中的键必须是不可变且唯一的,这意味着字符串、数字、元组等不可变对象可以作为键,但列表、字典等可变对象不可以作为键,而且字典中同一个键只能存在一个,如果有重复,后面的键会覆盖前面的键。
对于值没有约束,值可以是任意类型,可以是字符串、数字、列表、元组、字典、甚至是函数等。
如果你熟悉JSON,你会发现Python中的字典和JSON差不多,但两者其实还是有些不同的,比如字典在Python中是一种数据结构,它是内存中的对象,而JSON是一种数据交换格式,它本质上是一种字符串文本,还有就是字典的键和值支持比JSON要丰富的多,比如JSON的键是字符串,而Python的字典除了可以是字符串,还可以是数字、元组等。
创建一个空的字典,在Python中有两种常见的方式,直接使用{}或dict()构造函数:
empty_dic1 = {}empty_dic2 = dict()多数情况下我们直接使用{}来创建字典即可。创建非空字典时,我们只需要直接写键和值即可:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}在Python中访问字典中的数据通过[]或get()方法来取值,比如我们要访问字典d中的键为name的值 :
print(d['name']) # 输出:张三 print(d.get('name')) # 输出:张三对于比较复杂点的字典,比如字典中嵌套有字典数据,那就多套一层[]或者多一层get()方法,比如访问字典d中键为python的值:
print(d["score"]["python"]) # 输出:88print(d.get("score").get("python")) # 输出:88在字典中取值时,如果指定的键不存在,使用[]和get()方法表现是不一样的,使用[]访问不存在的键时,程序会出现KeyError异常,而get()方法则会输出None。使用get()方法有一个好处是对于不存在的键时,我们可以这是一个默认值:
print(d.get("score", {}).get("english", 0)) # 输出:0如果我们想在字典d中新增键值对或者修改已有键对应的值,也是用[],这和访问字典的方法类似,不同的是访问字典不需要赋值,新增或修改字典是需要赋值,比如我们要为字典d新增一个age = 20键值对:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d['age'] = 20print(d) # 输出:{'id': 1, 'name': '张三', 'score': {'math': 90, 'python': 88}, 'age': 20}对于有嵌套的情况,和访问字典一样,多套一层[]即可。
对于已经存在的键值对使用[]则是修改,比如修改字典d中的name的值:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d["name"] = "李四"print(d) # 输出:{'id': 1, 'name': '李四', 'score': {'math': 90, 'python': 88}}修改字典还有一种方式是用字典中的update()方法,它的好处在于可以进行批量修改,比如我们可以对字典d同时进行新增age和修改name:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d.update({"age": 20, "name": "李四"})print(d) # 输出:{'id': 1, 'name': '李四', 'score': {'math': 90, 'python': 88}, 'age': 20}上面的代码我们在update()方法中直接传入了字典类型的数据,也可以通过关键字的形式传入:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d.update(age = 20, name = "李四")print(d) # 输出:{'id': 1, 'name': '李四', 'score': {'math': 90, 'python': 88}, 'age': 20}如果我们想删除字典中的某个键值对,可以使用del语句,比如删除字典d中的id键值对:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}del d["id"]print(d) # 输出:{'name': '张三', 'score': {'math': 90, 'python': 88}}也可以使用pop()方法来删除:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}v = d.pop("id")print(v) # 输出:1print(d) # 输出:{'name': '张三', 'score': {'math': 90, 'python': 88}}v1 = d.pop("id", "不存在")print(v1) # 输出:不存在使用pop()方法的一个特点是删除指定键时会有返回值,还有就是可以设置默认值,这是一种比较安全的删除方法,因为如果指定的键不存在,则会报KeyError,这和使用del语句删除不存在的键时表现是一样的。
上面使用的del语句和pop()方法一次只能删除一个键值对,如果想批量清空字典,可以使用字典的clear()方法:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d.clear()print(d) # 输出:{}当然,del语句其实也可以删除字典对象,只是它删除对象后后续的代码再访问字典就会出现xx异常:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}del dprint(d) # 会出现NameError: name 'd' is not defined. 遍历字典中的键和值是日常开发中较为常见的操作,使用for...in结合一些字典中的方法可以帮助我们实现这一点。比如我现在想获取字典d中的键都有哪些:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}for k in d: print(k)上面的代码会输出这样的内容:
idnamescore我们也可以用字典的keys()方法来输出字典d中所有的键:
print(list(d.keys()))为了更便于阅读,我们使用list()构造方法将结果转换为了列表。
我们也可以遍历字典d中所有的值,使用values()方法:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}for v in d.values(): print(v)上面的代码会输出这样的内容:
1张三{'math': 90, 'python': 88}当然,多数情况下,我们可能希望键和值同时遍历,可以用字典中的items()方法:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}for k,v in d.items(): print(f"{k}={v}")上面的代码会输出这样的内容:
id=1name=张三score={'math': 90, 'python': 88}和列表一样,字典的拷贝也分为浅拷贝和深拷贝,浅拷贝就是使用字典中的copy()方法,深拷贝和列表一样,也使用copy模块中的deepcopy()方法。
比如我们复制一份字典d并对它进行修改:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d1 = d.copy()d1["score"]["math"] = 80print(d) # 输出:{'id': 1, 'name': '张三', 'score': {'math': 80, 'python': 88}}print(d1) # 输出:{'id': 1, 'name': '张三', 'score': {'math': 80, 'python': 88}}使用字典的copy()方法属于浅拷贝,对于副本字典d1中嵌套的score字典的修改就会导致原始数据跟着修改,如果不想修改原始数据就需要使用深拷贝:
import copyd = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d1 = copy.deepcopy(d)d1["score"]["math"] = 80print(d) # 输出:{'id': 1, 'name': '张三', 'score': {'math': 90, 'python': 88}}print(d1) # 输出:{'id': 1, 'name': '张三', 'score': {'math': 80, 'python': 88}}和列表推导式一样,字典中也可以使用推导式,它可以帮助我们快速创建或转换字典,基本语法与列表稍有不同,因为字典中是键值对,但大致是差不多的,我们来看一下基本语法:
{键表达式:值表达式 for 变量 in 可迭代对象 if 条件表达式}比如我们可以使用字典推导式结合zip()方法将两个列表快速组合成一个字典结构:
ids = [1, 2, 3]names = ['张三', '李四', '王五']d = {id: name for id, name in zip(ids, names)}print(d) # 输出:{1: '张三', 2: '李四', 3: '王五'}zip()方法能够帮助我们将两个列表的对应位置配成一对,打包成元组,类似这样:[(1, '张三'), (2, '李四'), (3, '王五')]。
使用字典推导式筛选元素也是比较简洁,比如筛选字典d中score大于或等于90的科目:
d = { "id": 1, "name": "张三", "score": { "math": 90, "python": 88 }}d = {k: v for k, v in d["score"].items() if v >= 90 }print(d) # 输出:{'math': 90}集合是一种基于哈希表实现的、无序、不可重复、可变的容器型数据结构。它比较核心的一个特点是存储的元素是唯一的,而且支持高效的成员检测和数学运算(交集、并集等)。
集合也是用{}来定义,和字典一样,但不同的是字典是键值对,而集合是单个独立的元素,我们来定义一个集合:
s = {1, 2, 3, 4}但如果我们想定义一个空的集合,则不能直接使用{},因为这会被识别为字典类型,这一点我们很容易验证:
s = {}print(type(s)) # <class 'dict'>那如何创建一个空的集合呢?
使用set()构造方法:
s = set()print(type(s)) # 输出:<class 'set'>需要注意的一点是,集合并不是支持存储任意类型的,因为它是基于哈希表实现的,这意味着可变类型是不能存储的,因为可变类型的哈希值会随着内容的变化而变化,这也就是说像列表、字典、集合、可变元组(如元组中包含列表)、自定义的可变对象都是不能存储的。
比如像下面这些集合都不合法:
s1 = {[1, 2]} # 元素是列表s2 = {{"id": 1}} # 元素是字典s3 = {{1, 2}} # 元素是集合s4 = {(1, [2, 3])} # 元素是元组但包含可变的列表集合与列表、元组不同,列表和元组可以通过索引来访问,但集合不可以,它只能通过in或not in关键字来判断元素是否存在:
s = {1, 2, 3}print(1 in s) # 输出:Trueprint(4 not in s) # 输出:True或者通过for循环来遍历集合中的元素:
s = {1, 2, 3}for elem in s: print(elem)上面的代码会输出这样的内容:
123如果我们需要往已存在的集合中添加元素,集合中提供了add(),比如在集合s = {1, 2, 3}中新增一个元素:
s = {1, 2, 3}s.add(4)print(s) # 输出:{1, 2, 3, 4}由于集合中存储的元素都是唯一的,所以如果待添加的元素,集合中已经存在,则不会有任何变化,比如在集合s = {1, 2, 3}新增元素3:
s = {1, 2, 3}s.add(3)print(s) # 输出:{1, 2, 3}如果想批量添加元素,可以使用update()方法,比如在集合s = {1, 2, 3}中添加一组元素:
s = {1, 2, 3}s.update([4, 5])print(s) # 输出:{1, 2, 3, 4, 5}我们在前面提到创建集合时是不能使用可变对象的,比如列表,但在使用update()方法时,是可以使用列表这种可变对象的,因为列表在这里只是载体,并不是直接存入的,它会遍历每一个元素添加到集合中。
update()方法接收的参数是一个可迭代对象,也就是说不仅仅是可以传入列表,也可以是元组,也可以是集合,也可以是字典。对于字典类型的参数,update()方法默认使用的是字典的键。
如果我们想从集合s = {1, 2, 3}中删除一个元素,可以使用remove()方法:
s = {1, 2, 3}s.remove(3)print(s) # 输出:{1, 2}但remove()方法在删除不存在的元素时会出现KeyError异常,我们可以使用discard()方法来避免这个问题:
s = {1, 2, 3}s.discard(4)print(s) # 输出:{1, 2, 3}集合删除元素中还有一个有意思的方法:pop(),它可以随机删除一个元素并返回这个元素的值,比如对于下面的代码,你执行多次打印的结果可能是不同的:
s = {'a', 'b', 'c'}s.pop()print(s) # 输出可能是{'a', 'c'},也可能是{'c', 'b'}或者{'c', 'a'}如果需要返回结果,可以使用一个变量来接收pop()方法的返回值。
集合也有一个和字典相同的清空方法:clear(),我们可以用它清空整个集合:
s = {1, 2, 3}s.clear()print(s) # 输出:set()Python中的集合还有一个核心的用法,那就是它天然支持数学中的交集、并集、差集、对称集等操作。
比如现在有两个集合a = {1, 2, 3},b = {2, 3, 4}。交集就是集合a和b中都有的元素,用&或intersection()方法实现:
a = {1, 2, 3}b = {2, 3, 4}print(a & b) # 输出:{2, 3}print(a.intersection(b)) # 输出:{2, 3}并集就是集合a和b中都有的元素,重复元素只会存在一个,用|或union()方法实现:
print(a | b) # 输出:{1, 2, 3, 4}print(a.union(b)) # 输出:{1, 2, 3, 4}差集就是集合a中有,但b中没有的元素,用-或difference()方法实现:
print(a - b) # 输出:{1}print(a.difference(b)) # 输出:{1}对称集就是取集合a和b中互不包含的元素,用^或symmetric_difference()方法实现:
print(a ^ b) # 输出:{1, 4}print(a.symmetric_difference(b)) # 输出:{1, 4}除了上面一些操作,集合中还包含一些判断方法,比如issubset()方法用于判断是否为子集,假定现在有一个新的集合c = {1, 2},我们可以用issubset()方法来判断集合c是否为a的子集:
a = {1, 2, 3}c = {1, 2}print(c.issubset(a)) # 输出:True还有issuperset()方法用于判断是否为超集,isdisjoint()方法用于判断无交集判断,这里就不再一一举例。
集合的数学运算有比较常见的一些适用场景,比如可以用并集来做多组数据的去重、可以用交集筛选多组数据重合的部分等等。
像列表、字典一样,集合也可以用推导式,它的基本语法和字典类似,只是少了字典中的值,基本的语法是这样的:
{表达式 for 变量 in 可迭代对象 if 条件表达式}其中的if条件表达式是可选的。
比如我们用推导式来实现集合a和b的交集,可以这么做:
a = {1, 2, 3}b = {2, 3, 4}c = {x for x in a if x in b}print(c) # 输出:{2, 3}我们在前面聊集合创建时说到集合中是不能存储集合的,比如像这样一个集合s = {{1, 2}}就是不合法的,因为集合要求它的元素是不可变、可哈希。但如果我们的集合就是不可变的集合,那是不是就可以了?
是的,Python中就有这样的不可变集合,就可以存储到集合中,我们可以用frozenset()构造函数来创建不可变集合。
使用不可变集合作为普通集合的元素就可以这么做:
s = frozenset({1, 2})s1 = {s}既然是不可变集合,就会有一些限制,这意味着不可变集合只能使用集合中的可读性的操作,比如判断成员的in或not in是可以用的,数学运算(交集、并集、差集等)也都是可以用的。
不可变集合让我想到了另外一件事,就是字典中的键也是只能使用不可变对象,普通的集合是不能作为键的,但是现在不可变集合则可以作为字典的键。
列表、元组、字典、集合这几种数据结构有时候需要从某一种数据结构转换为另一种数据结构,这样会让我们操作更容易,比如列表数据我们需要去重,就可以将列表转换为集合,自动帮我们去重;再比如元组不可修改,我们可以将其转换为列表,修改后在转回元组;再比如字典中我们只需要键或值,可以转换为列表等等,这些转换在Python中也比较容易,一般可以通过对应的构造函数实现,比如list()、dict()、tuple()、set()。
当然像列表、元组、集合往字典转换需要满足键值对的格式,以列表为例,可以是嵌套的二维列表,子列表中的元素成对出现:
a = [[1, 'a'], [2, 'b']]d = dict(a)print(d) # 输出:{1: 'a', 2: 'b'}或者可以是两个列表,通过zip()来辅助构建:
a = [1, 2]b = ['a', 'b']d = dict(zip(a, b))print(d) # 输出:{1: 'a', 2: 'b'}而字典向列表、元组、集合转换则相对容易,它们默认取字典的键,我们也可以通过字典的values()方法来取字典中的值,以字典转换为列表为例:
d = {1: 'a', 2: 'b'}a = list(d)print(a) # 输出:[1, 2]列表、元组、字典、集合这些数据结构属于Python中原生数据结构,具有通用性,能够满足基本的需求,但在一些场景下存在效率低、使用不便捷的问题。
collections模块是对原生数据结构的增强,能够帮助我们解决原生数据结构的一些痛点,接下来我们重点看一下collections中一些常用的数据结构,如:deque、namedtuple、defaultdict、OrderedDict、Counter。
说到数据结构,其实往往是和算法分不开的,在列表中如果我们想在头部插入元素,可以用insert()方法,列表本质上是动态数组,当我们使用insert()方法在头部插入元素时,需要移动所有的元素,时间复杂度为O(n),数据量较大时效率就会变的极差,collections模块中的deque就可以帮助我们解决这个问题。
deque也叫双端队列,是基于双向链表实现,在头部和尾部操作的时间复杂度都是O(1),也就是说它的处理时间不会随着数据量变大而变长。
创建一个deque对象需要使用deque()构造函数:
from collections import dequed = deque()print(d) # 输出:deque([])d1 = deque([1, 2, 3])print(d1) # 输出:deque([1, 2, 3])d2 = deque((1, 2, 3))print(d2) # 输出:deque([1, 2, 3])d3 = deque({1, 2, 3})print(d3) # 输出:deque([1, 2, 3])d4 = deque("123")print(d4) # 输出:deque(['1', '2', '3'])deque()构造函数可以不传参数,就创建一个空的deque对象,或者传入可迭代对象(列表、元组、集合、字符串等)。
我们在初始化deuqe的时候还可以使用maxlen来指定队列的最大长度,添加元素如果超出长度时,会从相反的方向删除元素,比如从头部添加元素,超出长度则从尾部删除元素,如果从尾部添加元素,超出长度时则从头部删除元素。
如果我们想在队列的头部添加元素,使用appendleft()方法:
from collections import dequed = deque([1, 2, 3])d.appendleft(0)print(d) # deque([0, 1, 2, 3])从尾部追加元素则和列表方法一样,使用append()方法。
头部删除元素则用popleft():
from collections import dequed = deque([1, 2, 3])d.popleft()print(d) # 输出:deque([2, 3])尾部删除则和列表一样,也是用pop()方法,popleft()和pop()方法都是有返回值的,如果需要使用返回值,可以使用变量接收。
如果需要批量在队列的头部添加元素,可以使用extendleft()方法:
from collections import dequed = deque([4, 5])d.extendleft([1, 2, 3])print(d) # deque([3, 2, 1, 4, 5])从尾部追加元素也和列表一样使用extend()方法。
你可能已经发现了,在头部批量添加元素是逆序的,还有就是这几个方法头部方法与尾部方法的不同就是多了一个left,从左侧,也代表从头部操作元素。
deque中还有一些其他的方法,比如rotate()方法、clear()方法、remove()方法等,这里就不再一一介绍,在使用的时候可以根据情况查阅相关用法。
deque虽然与列表有很多相似的操作,但也并非全面优于列表,它在队列、栈、滑动窗口、轮训任务等以头尾增删为主的场景中有广泛应用,在需要频繁随机访问、切片、排序等方面还是列表比较有优势。
如果我们想表达一个学生的信息,可以用元组来这么表达:
student = (1, "张三", 20, 90)这里面意味着,我们要知道索引第0个元素表示id,第1个元素表示姓名,第2个元素表示年龄,第3个元素表示某科目的成绩,一旦把索引对应关系记错就可能出现不易排查的问题,如果能有属性名来标记那就容易记多了。
Python中也确实有这样的数据结构来帮助我们解决这样的问题,那就是namedtuple,可命名元组。
所谓命名元组就是让元组和元素具有语义化,通过属性名代替索引,可读性增强。
我们来看一下使用namedtuple如何来定义上面的student:
from collections import namedtupleStudent = namedtuple('Student',['id', 'name', 'age', 'score'])s = Student(1, '张三', 20, 90)print(s.id) # 输出:1print(s[0]) # 输出:1namedtuple可以使用属性名访问,也兼容索引访问。这看起来像是定义了一个类,但它比自定义类更简洁,无需写__init__方法来初始化属性,它也比自定义类更轻量和占用内存更低,而且它具备元组的特性,还有一些非常使用的方法,比如将上面的元组s转换为字典:
d = s._asdict()print(d) # 输出:{'id': 1, 'name': '张三', 'age': 20, 'score': 90}如果想查看已创建的namedtuple有哪些字段,可以使用_fields属性。
假定现在有这么一个列表words = ['a', 'b', 'c', 'a', 'b'],我想统计每个字母出现的次数,如果使用字典来记录,我们可以这么写:
words = ['a', 'b', 'c', 'a', 'b']result = {}for w in words: result[w] += 1print(result)但这段代码是不能正常执行的,会出现KeyError异常,因为遍历words列表时,第一次出现的元素在result字典中是不存在的,所以抛出KeyError,也就是说我们需要判断键是否存在,那就用not in来改进一下:
words = ['a', 'b', 'c', 'a', 'b']result = {}for w in words: if w not in result: result[w] = 0 result[w] += 1print(result) # 输出:{'a': 2, 'b': 2, 'c': 1}如果我不想显式的判断键是否存在,可以使用get()方法来设置一个默认值:
words = ['a', 'b', 'c', 'a', 'b']result = {}for w in words: result[w] = result.get(w, 0) + 1print(result) # 输出:{'a': 2, 'b': 2, 'c': 1}还有一种方法就是本节要聊的defaultdict,也可以让我们不必担忧是否忘记判断键的存在,也不用设置默认值:
from collections import defaultdictwords = ['a', 'b', 'c', 'a', 'b']result = defaultdict(int)for w in words: result[w] += 1print(result) # 输出:defaultdict(<class 'int'>, {'a': 2, 'b': 2, 'c': 1})初始化defaultdict时需要指定一个工厂函数,这个工厂函数可以是内置类型,如int,默认值是0;如list,默认值是空列表[];如set,默认值是空集合set();如dict,默认值是空字典{};我们说defaultdict参数是一个工厂函数,指定的是这些int、list、set、dict,实际上是调用对应的函数,即int()、list()、set()、dict()。当然这个工厂函数也可以是自定义函数。
上面统计列表字符的例子可以说是小试牛刀,嵌套字典判空更是一个典型的应用场景。我们来看一个例子,比如现在有一组订单数据:
orders = [ ('北京市', '海淀区', 'A街道', 10), ('北京市', '海淀区', 'A街道', 10), ('北京市', '海淀区', 'B街道', 15), ('北京市', '朝阳区', 'C街道', 20), ('北京市', '朝阳区', 'C街道', 30)] 接下来我想对这个订单数据进行统计,汇总各街道订单数量,想输出的格式是这样的:
{ "北京市": { "海淀区": { "A街道": 20, "B街道": 15 }, "朝阳区": { "C街道": 50 } }}那我们就需要遍历订单数据,然后合并相同街道的订单数据,代码可以这样写:
result= {}for city, area, street, count in orders: if city not in result: result[city] = {} if area not in result[city]: result[city][area] = {} if street not in result[city][area]: result[city][area][street] = 0 result[city][area][street] += countprint(result) # 输出:{'北京市': {'海淀区': {'A街道': 20, 'B街道': 15}, '朝阳区': {'C街道': 50}}}这种写法比较符合直觉,但每层都要做判断,也挺麻烦的,如果使用get()方法,可以有一定的简化:
result = {}for city, area, street, count in orders: result[city] = result.get(city, {}) result[city][area] = result[city].get(area, {}) result[city][area][street] = result[city][area].get(street, 0) + countprint(result) # 输出:{'北京市': {'海淀区': {'A街道': 20, 'B街道': 15}, '朝阳区': {'C街道': 50}}}如果使用defaultdict会更加简洁:
from collections import defaultdictresult = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))for city, area, street, count in orders: result[city][area][street] += countprint(result)遍历orders列表时,一行代码就可以累加订单的数量,这里需要注意的是defaultdict(lambda: defaultdict(lambda: defaultdict(int))),我们使用了三层defaultdict,而且使用的是lambda匿名函数,这是因为我们每次调用默认值的时候都要保证产生的是一个新的实例,否则所有层都共用一个实例就会出现问题,实际上如果省略了lambda,程序也无法正常运行,会提示参数必须是可调用对象。
使用defaultdict,最终打印的结果是defaultdict类型,并不是对阅读比较友好的形式,我们需要转换成普通的字典,这里就可以用字典推导式来实现:
r = { city: { area: dict(street) for area, street in areas.items() } for city, areas in dict(result).items()}print(r) # 输出:{'北京市': {'海淀区': {'A街道': 20, 'B街道': 15}, '朝阳区': {'C街道': 50}}}这里的字典推导式因为有嵌套的问题,看起来有些不易理解,我们简单解释一下便于理解,先来回忆一下字典推导式的语法:
{键表达式:值表达式 for 变量 in 可迭代对象 if 条件表达式}根据这个语法我们将字典r简化一下:
r = { city: {} for city, areas in dict(result).items()}这样看起来是不是就容易理解一些了,只不过将for语句换行了。
理解了外层推导式,city对应的值的内层推导式其实也是一样的逻辑,这里面我们还用了dict()函数是将defaultdict转换为普通的字典操作。
从上面的例子中我们可以看到defaultdict对比普通的dict,帮我们解决的一个核心问题是访问不存在的键会出现KeyError的问题,我们在初始化时预设了一个工厂函数,从而帮助我们简化代码。
与字典相关的数据结构,在collections模块中还有一个OrderedDict,看着名字就能大概知道它是可以控制顺序的,它的核心功能也正是与此有关。
OrderedDict会严格记录键值对的插入顺序,也提供了一些专属的顺序操作方法,如果你对字典的插入顺序有比较强的需求可以考虑使用OrderedDict,这里不再做深入了解。
如果现在有一个列表elems = ['a', 'b', 'c', 'a', 'c', 'b', 'b', 'c'],我们想统计元素出现的次数,根据前面聊过的内容,可以使用defaultdict来实现:
from collections import defaultdictelems = ['a', 'b', 'c', 'a', 'c', 'b', 'b', 'c']result = defaultdict(int)for e in elems: result[e] += 1print(dict(result)) # 输出:{'a': 2, 'b': 3, 'c': 3}关于这种计数,其实在collections模块中提供了专门的数据结构来帮助我们更简单的处理问题,那就是Counter:
from collections import Counterelems = ['a', 'b', 'c', 'a', 'c', 'b', 'b', 'c']counter = Counter(elems)print(dict(counter)) # 输出:{'a': 2, 'b': 3, 'c': 3}Counter本质上是一个专门用于计数的字典子类,它能以比较简单的方式帮助我们统计可迭代对象(列表、字符串、元组等)中每个元素出现的次数。
它的能力还不止像上面的例子简单统计一个列表的元素中的数量,还提供了一些操作方法,比如我想找到列表中出现次数最多的两个元素:
from collections import Counterelems = ['b', 'c', 'd','a', 'b', 'a', 'c', 'd', 'a', 'b', 'a', 'a']counter = Counter(elems)res = counter.most_common(2)print(res) # 输出:[('a', 5), ('b', 3)]most_common()方法也可以不传入参数,返回的是按降序排序的所有元素。
如果有不止一个计数器,Counter之间还可以进行加减运算:
from collections import Counterelems1 = ['a', 'b', 'c', 'b', 'c']elems2 = ['a', 'b', 'c', 'd']counter1 = Counter(elems1)counter2 = Counter(elems2)res = counter1 + counter2print(dict(res)) # 输出:{'a': 2, 'b': 3, 'c': 3, 'd': 1}res2 = counter1 - counter2print(dict(res2)) # 输出:{'b': 1, 'c': 1}至此,我们已经将Python中比较常用的一些数据结构聊完了,接下来就需要通过练习来巩固一下知识点。
假定现在有这么一份模拟的学生考试成绩相关的数据:
# 成绩原始数据:每个元组格式为 (学生ID, 姓名, 科目, 分数, 考试类型(期中/期末))[ (101, "张三", "数学", 92, "期中"), (101, "张三", "语文", 85, "期中"), (101, "张三", "英语", 88, "期中"), (101, "张三", "数学", 95, "期末"), (101, "张三", "语文", 89, "期末"), (101, "张三", "英语", 90, "期末"), (102, "李四", "数学", 78, "期中"), (102, "李四", "语文", 94, "期中"), (102, "李四", "英语", 80, "期中"), (102, "李四", "数学", 82, "期末"), (102, "李四", "语文", 96, "期末"), (102, "李四", "英语", 85, "期末"), (103, "王五", "数学", 65, "期中"), (103, "王五", "语文", 75, "期中"), (103, "王五", "英语", 70, "期中"), (103, "王五", "数学", 72, "期末"), (103, "王五", "语文", 80, "期末"), (103, "王五", "英语", 78, "期末"), (104, "赵六", "数学", 98, "期中"), (104, "赵六", "语文", 88, "期中"), (104, "赵六", "英语", 95, "期中"), (104, "赵六", "数学", 96, "期末"), (104, "赵六", "语文", 90, "期末"), (104, "赵六", "英语", 97, "期末"), (105, "孙七", "数学", 85, "期中"), (105, "孙七", "语文", 82, "期中"), (105, "孙七", "英语", 86, "期中"), (105, "孙七", "数学", 88, "期末"), (105, "孙七", "语文", 84, "期末"), (105, "孙七", "英语", 89, "期末")]处理这份数据,希望你做到以下几点:1)找出所有考试的科目名称2)统计每个学生每科期中、期末的平均分,所有科目的总分(期中+期末),个人总成绩排名3)找出分数出现次数最多的前5个
尽量用本章所学到的知识点来解决这三个问题。当然,如果你有自己的想法或问题要处理更好。