基础数据类型:
- int,整数类型(整形)
- float,浮点类型(浮点型)
- complex,复数类型
- bool,布尔类型
- str,字符串类型
高级数据类型:
- list,列表类型
- tuple,元组类型
- dict,字典类型
- set,集合类型
学习目标:掌握列表的各种操作
本节问题:
- 问题1:列表的定义是什么?有哪些特点?
- 问题2:列表支持哪些方法和运算?
- 问题3:列表如何对元素进行增删改查?列出具体的方法或者基于的运算。
- 问题4:如何对列表元素进行遍历?
- 问题5:列表生成式和生成器的语法是什么?简单说一下它们的区别。
不知道大家是否注意到,字符串类型(str)和之前讲到的数值类型(int和float)有一些区别。数值类型是标量类型,也就是说这种类型的对象没有可以访问的内部结构;而字符串类型是一种结构化的、非标量类型,所以才会有一系列的属性和方法。接下来我们要介绍的列表(list),也是一种结构化的、非标量类型,它是值的有序序列,每个值都可以通过索引进行标识,定义列表可以将列表的元素放在[]中,多个元素用,进行分隔,可以使用for循环对列表元素进行遍历,也可以使用[]或[:]运算符取出列表中的一个或多个元素。
案例引入
在开始本节课的内容之前,我们先给大家一个编程任务,将一颗色子掷6000次,统计每个点数出现的次数。这个任务对大家来说应该是非常简单的,我们可以用1到6均匀分布的随机数来模拟掷色子,然后用6个变量分别记录每个点数出现的次数,相信大家都能写出下面的代码。
import random
f1 = 0
f2 = 0
f3 = 0
f4 = 0
f5 = 0
f6 = 0
for _ in range(6000):
face = random.randint(1, 6)
if face == 1:
f1 += 1
elif face == 2:
f2 += 1
elif face == 3:
f3 += 1
elif face == 4:
f4 += 1
elif face == 5:
f5 += 1
else:
f6 += 1
print(f'1点出现了{f1}次')
print(f'2点出现了{f2}次')
print(f'3点出现了{f3}次')
print(f'4点出现了{f4}次')
print(f'5点出现了{f5}次')
print(f'6点出现了{f6}次')
看看上面的代码,相信大家一定觉得它非常的“笨重”和“丑陋”,更可怕的是,如果要统计掷2颗或者更多的色子统计每个点数出现的次数,那就需要定义更多的变量,写更多的分支结构。讲到这里,相信大家一定想问:有没有办法用一个变量来保存多个数据,有没有办法用统一的代码对多个数据进行操作?答案是肯定的,在Python中我们可以通过容器类型的变量来保存和操作多个数据,我们首先为大家介绍列表(list)这种新的数据类型。
列表的定义
在Python中,列表是由一系元素按特定顺序构成的数据序列,这样就意味着定义一个列表类型的变量,可以保存多个数据,而且允许有重复的数据。跟我们讲到的字符串类型一样,列表也是一种结构化的、非标量类型。
在Python中,可以使用[]字面量语法来定义列表,列表中的多个元素用逗号进行分隔,代码如下所示。
items0 = [] # 空列表
items1 = [35, 12, 99, 68, 55, 87]
items2 = ['Python', 'Java', 'Go', 'Kotlin']
除此以外,还可以通过Python内置的list函数将其他序列变成列表。准确的说,list并不是一个函数,而是创建列表对象的构造器(后面会讲到对象和构造器这两个概念)。
items0 = list() # 空列表
items1 = list(range(1, 10))
print(items1) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
items2 = list('hello')
print(items2) # ['h', 'e', 'l', 'l', 'o']
需要说明的是,列表是一种可变数据类型,也就是说列表可以添加元素、删除元素、更新元素,这一点跟我们上一课讲到的字符串有着鲜明的差别。字符串是一种不可变数据类型,也就是说对字符串做拼接、重复、转换大小写、修剪空格等操作的时候会产生新的字符串,原来的字符串并没有发生任何改变。因此,列表的定义可以总结成这样一句话:列表是一个有序且可变的容器,在里面可以存放多个不同类型、数据元素可重复的元素。
- 特点
- 数据元素的类型可以不相同
- 以
[]包裹,,为分隔符 - 内部元素有序
- 内部元素可变
- 内部元素可重复
列表的方法
Python中为所有的列表类型的数据提供了一批内置方法。
在开始学习列表的内置方法之前,先来做一个字符串和列表的对比:
字符串,不可变,即:创建好之后内部就无法修改。【独有功能都是新创建一份数据】
name = "Michael" data = name.upper() print(name) print(data)列表,可变,即:创建好之后内部元素可以修改。【独有功能基本上都是直接操作列表内部,不会创建新的一份数据】
user_list = ["车子","房子"] user_list.append("票子") print(user_list) # ["车子","房子","票子"]
列表中的常见内置方法如下:
- 追加元素
- append():在原列表中尾部追加值。
- extend(iterable):批量追加, 将一个列表中的元素逐一添加另外一个列表
- insert(index, object):在原列表的指定索引位置插入值
- 修改元素
- list[index] = value:修改指定索引位置的元素的值为value
- 删除元素
- remove(value):在原列表中根据值删除(从左到右找到第一个删除)【慎用,里面没有会报错】
- pop(index):在原列表中根据索引踢出某个元素(根据索引位置删除),返回该元素的值
- del list[index]:根据索引删除某个元素
- 清空表
- clear():清空原列表
- 查询
- index(value):根据值获取索引 找不到会报错
- count(value):根据值获取出现频次
- 排序
- sort(reverse=False):排序
- reverse():反转列表
添加、删除、修改和清空列表
items = ['Python', 'Java', 'Go', 'Kotlin']
# 使用append方法在列表尾部添加元素
items.append('Swift')
print(items) # ['Python', 'Java', 'Go', 'Kotlin', 'Swift']
# 使用insert方法在列表指定索引位置插入元素
items.insert(2, 'SQL')
print(items) # ['Python', 'Java', 'SQL', 'Go', 'Kotlin', 'Swift']
# 使用extend方法将另一个列表添加该列表之后
items2 = ['C', 'C++', 'C#', 'Vue']
items.extend(items2)
print(items) # ['Python', 'Java', 'SQL', 'Go', 'Kotlin', 'Swift', 'C', 'C++', 'C#', 'Vue']
# 修改指定索引位置的元素
items[2] = 'MySQL'
items[-1] = 'React'
print(items) # ['Python', 'Java', 'MySQL', 'Go', 'Kotlin', 'Swift', 'C', 'C++', 'C#', 'React']
# 删除指定的元素 在原列表中根据值删除(从左到右找到第一个删除)【慎用,里面没有会报错】
items.remove('Java')
print(items) # ['Python', 'MySQL', 'Go', 'Kotlin', 'Swift', 'C', 'C++', 'C#', 'React']
# 删除指定索引位置的元素
items.pop(0)
items.pop(len(items) - 1)
print(items) # ['MySQL', 'Go', 'Kotlin', 'Swift', 'C', 'C++', 'C#']
# 清空列表中的元素
items.clear()
print(items) # []
需要提醒大家,在使用remove方法删除元素时,如果要删除的元素并不在列表中,会引发ValueError异常,错误消息是:list.remove(x): x not in list。在使用pop方法删除元素时,如果索引的值超出了范围,会引发IndexError异常,错误消息是:pop index out of range。
从列表中删除元素其实还有一种方式,就是使用Python中的del关键字后面跟要删除的元素,这种做法跟使用pop方法指定索引删除元素没有实质性的区别,但后者会返回删除的元素,前者在性能上略优(del对应字节码指令是DELETE_SUBSCR,而pop对应的字节码指令是CALL_METHOD和POP_TOP)。
items = ['Python', 'Java', 'Go', 'Kotlin']
del items[1]
print(items) # ['Python', 'Go', 'Kotlin']
查找元素位置和次数
列表类型的index方法可以查找某个元素在列表中的索引位置;因为列表中允许有重复的元素,所以列表类型提供了count方法来统计一个元素在列表中出现的次数。请看下面的代码。
items = ['Python', 'Java', 'Java', 'Go', 'Kotlin', 'Python']
# 查找元素的索引位置
print(items.index('Python')) # 0
print(items.index('Python', 2)) # 5
# 注意:虽然列表中有'Java',但是从索引为3这个位置开始后面是没有'Java'的
print(items.index('Java', 3)) # ValueError: 'Java' is not in list
再来看看下面这段代码。
items = ['Python', 'Java', 'Java', 'Go', 'Kotlin', 'Python']
# 查找元素出现的次数
print(items.count('Python')) # 2
print(items.count('Go')) # 1
print(items.count('Swfit')) # 0
元素排序和反转
列表的sort操作可以实现列表元素的排序,而reverse操作可以实现元素的反转,代码如下所示。
items = ['Python', 'Java', 'Go', 'Kotlin', 'Python']
# 排序
items.sort()
print(items) # ['Go', 'Java', 'Kotlin', 'Python', 'Python']
# 反转
# items.sort(reverse=True)
items.reverse()
print(items) # ['Python', 'Python', 'Kotlin', 'Java', 'Go']
# 注意:排序时内部元素无法进行比较时,程序会报错(尽量数据类型统一)。
列表的运算
和字符串类型一样,列表也支持拼接、重复、成员运算、索引和切片以及比较运算,这里将列表常见的运算总结如下,请大家参考。
[] + [], 相加,两个列表相加生成新列表[] * int,相乘,列表*整型 将列表中的元素再创建N份并生成一个新的列表in/not in,成员运算,判断元素是否在列表中len, 获取列表的长度[index],索引[start:end:step],切片比较运算:比的是对应索引位置上的元素的大小
使用示例
items1 = [35, 12, 99, 68, 55, 87]
items2 = [45, 8, 29]
# 列表的拼接
items3 = items1 + items2
print(items3) # [35, 12, 99, 68, 55, 87, 45, 8, 29]
# 列表的重复
items4 = ['hello'] * 3
print(items4) # ['hello', 'hello', 'hello']
# 列表的成员运算
print(100 in items3) # False
print('hello' in items4) # True
# 获取列表的长度(元素个数)
size = len(items3)
print(size) # 9
# 列表的索引
# 查
print(items3[0], items3[-size]) # 35 35
items3[-1] = 100
print(items3[size - 1], items3[-1]) # 100 100
# print(items3[20]) # 报错
# 改
# items3[1] = 20
# print(items3) # [35, 12, 99, 68, 55, 87, 45, 8, 29]
# 删 比较一下这种方法与remove和pop的区别
# del items[1]
# print(items3) # [35, 99, 68, 55, 87, 45, 8, 29]
# 注意:超出索引范围会报错。
# 提示:由于字符串是不可变类型,所以他只有索引读的功能,而列表可以进行 读、改、删
# 列表的切片 也支持读、改、删
# 读
print(items3[:5]) # [35, 12, 99, 68, 55]
print(items3[4:]) # [55, 87, 45, 8, 100]
print(items3[-5:-7:-1]) # [55, 68]
print(items3[::-2]) # [100, 45, 55, 99, 35]
print(items3[::-1]) # 反转列表
# 改
# items3[1:3] = [1,10]
# print(items) # [35, 1, 10, 68, 55, 87, 45, 8, 29]
# 删
# del items3[1:]
# print(items) # [35]
# 列表的比较运算
items5 = [1, 2, 3, 4]
items6 = list(range(1, 5))
# 两个列表比较相等性比的是对应索引位置上的元素是否相等
print(items5 == items6) # True
items7 = [3, 2, 1]
# 两个列表比较大小比的是对应索引位置上的元素的大小
print(items5 <= items7) # True
要注意的是,由于列表是可变类型,所以通过索引操作既可以获取列表中的元素,也可以更新列表中的元素。对列表做索引操作一样要注意索引越界的问题,对于有N个元素的列表,正向索引的范围是0到N-1,负向索引的范围是-1到-N,如果超出这个范围,将引发IndexError异常,错误信息为:list index out of range。
列表元素的遍历
如果想逐个取出列表中的元素,可以使用for循环的,有以下两种做法。
方法一:
items = ['Python', 'Java', 'Go', 'Kotlin']
for index in range(len(items)):
print(items[index])
方法二:
items = ['Python', 'Java', 'Go', 'Kotlin']
for item in items:
print(item)
讲到这里,我们可以用列表的知识来重构上面“掷色子统计每个点数出现次数”的代码。
import random
counters = [0] * 6
for _ in range(6000):
face = random.randint(1, 6) # face = 5
counters[face - 1] += 1 # [0, 0, 0, 0, 1, 0]
for i in range(1, 7):
print(f'点数 {i} 出现了 {counters[i - 1]} 次')
for i in range(len(counters)):
print(f'点数 {i + 1} 出现了 {counters[i]} 次')
for index, value in enumerate(counters):
print(f'点数 {index + 1} 出现了 {value} 次')
上面的代码中,我们用counters列表中的六个元素分别表示1到6的点数出现的次数,最开始的时候六个元素的值都是0。接下来用随机数模拟掷色子,如果摇出1点counters[0]的值加1,如果摇出2点counters[1]的值加1,以此类推。大家感受一下,这段代码是不是比之前的代码要简单优雅很多。
切记,循环的过程中对数据进行删除会踩坑
错误方式:有坑
# 错误方式, 有坑,结果不是你想要的。 user_list = ["刘德发", "范德彪", "刘华强", '刘尼古拉斯赵四', "宋小宝", "刘能"] for item in user_list: if item.startswith("刘"): user_list.remove(item) print(user_list)正确方式:倒着删除
user_list = ["刘得发", "范德彪", "刘华强", '刘尼古拉斯赵四', "宋小宝", "刘能"] for index in range(len(user_list) - 1, -1, -1): item = user_list[index] if item.startswith("刘"): user_list.remove(item) print(user_list)
列表类型转换
其他数据类型转化为列表。只需要在外面包裹一层list即可。int、bool无法转换成列表
list(其他类型数据) # 其他数据类型可以是str/tuple/dict/set等
示例:
# int、bool无法转换成列表
# str -> list
name = "张亚飞"
data = list(name) # ["张","亚","飞"]
print(data)
# tuple -> list
v1 = (11,22,33,44) # 元组
vv1 = list(v1) # 列表 [11,22,33,44]
# set -> list
v2 = {"Michael","james","kobe"} # 集合
vv2 = list(v2) # 列表 ["Michael","james","kobe"]
# dict -> list
v1 = {'k1': 'v1', 'k2': 'v2'}
v2 = list(v1) # ['k1', 'k2']
v3 = list(v1.keys()) # ['k1', 'k2']
v4 = list(v1.values()) # ['v1', 'v2']
# 其他可迭代对象
v1 = list(range(10))
嵌套列表
Python语言没有限定列表中的元素必须是相同的数据类型,也就是说一个列表中的元素可以任意的数据类型,当然也包括列表。如果列表中的元素又是列表,那么我们可以称之为嵌套的列表。嵌套的列表可以用来表示表格或数学上的矩阵,例如:我们想保存5个学生3门课程的成绩,可以定义一个保存5个元素的列表保存5个学生的信息,而每个列表元素又是3个元素构成的列表,分别代表3门课程的成绩。但是,一定要注意下面的代码是有问题的。
scores = [[0] * 3] * 5
print(scores) # [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
看上去我们好像创建了一个5 * 3的嵌套列表,但实际上当我们录入第一个学生的第一门成绩后,你就会发现问题来了,我们看看下面代码的输出。
# 嵌套的列表需要多次索引操作才能获取元素
scores[0][0] = 95
print(scores) # [[95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0], [95, 0, 0]]
我们不去过多的解释为什么会出现这样的问题,如果想深入研究这个问题,可以通过Python Tutor网站的可视化代码执行功能,看看创建列表时计算机内存中发生了怎样的变化,下面的图就是在这个网站上生成的。建议大家不去纠结这个问题,现阶段只需要记住不能用[[0] * 3] * 5]这种方式来创建嵌套列表就行了。那么创建嵌套列表的正确做法是什么呢,下面的代码会给你答案。
scores = [[0for _ in range(5)]
scores[0][0] = 95
print(scores) # [[95, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
对于嵌套的值,可以根据之前学习的索引知识点来进行学习,例如:
data = [ "谢广坤",["海燕","赵本山"],True,[11,22,33,44],"宋小宝" ]
print( data[0] ) # "谢广坤"
print( data[1] ) # ["海燕","赵本山"]
print( data[0][2] ) # "坤"
print( data[1][-1] ) # "赵本山"
data.append(666)
print(data) # [ "谢广坤",["海燕","赵本山"],True,[11,22,33,44],"宋小宝",666]
data[1].append("谢大脚")
print(data) # [ "谢广坤",["海燕","赵本山","谢大脚"],True,[11,22,33,44],"宋小宝",666 ]
del data[-2]
print(data) # [ "谢广坤",["海燕","赵本山","谢大脚"],True,[11,22,33,44],666 ]
data[-2][1] = "Michael"
print(data) # [ "谢广坤",["海燕","赵本山","谢大脚"],True,[11,"Michael",33,44],666 ]
data[1][0:2] = [999,666]
print(data) # [ "谢广坤",[999,666,"谢大脚"],True,[11,"Michael",33,44],666 ]
列表生成式
在Python中,列表还可以通过一种特殊的字面量语法来创建,这种语法叫做生成式。它的语法是:
[exp for iter_var in iterable]
- 工作过程
- 迭代iterable中的每个元素;
- 每次迭代都先把结果赋值给iter_var,然后通过exp得到一个新的计算值;
- 最后把所有通过exp得到的计算值以一个新列表的形式返回。
我们给出两段代码,大家可以做一个对比,看看哪一种方式更加简单优雅。
通过for循环为空列表添加元素。
# 创建一个由1到9的数字构成的列表
items1 = []
for x in range(1, 10):
items1.append(x)
print(items1)
# 创建一个由'hello world'中除空格和元音字母外的字符构成的列表
items2 = []
for x in 'hello world':
if x not in ' aeiou':
items2.append(x)
print(items2)
# 创建一个由个两个字符串中字符的笛卡尔积构成的列表
items3 = []
for x in 'ABC':
for y in '12':
items3.append(x + y)
print(items3)
通过生成式创建列表。
# 创建一个由1到9的数字构成的列表
items1 = [x for x in range(1, 10)]
print(items1) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 创建一个由'hello world'中除空格和元音字母外的字符构成的列表
items2 = [x for x in 'hello world' if x not in ' aeiou']
print(items2) # ['h', 'l', 'l', 'w', 'r', 'l', 'd']
# 创建一个由个两个字符串中字符的笛卡尔积构成的列表
items3 = [x + y for x in 'ABC' for y in '12']
print(items3) # ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
下面这种方式不仅代码简单优雅,而且性能也优于上面使用for循环和append方法向空列表中追加元素的方式。可以简单跟大家交待下为什么生成式拥有更好的性能,那是因为Python解释器的字节码指令中有专门针对生成式的指令(LIST_APPEND指令);而for循环是通过方法调用(LOAD_METHOD和CALL_METHOD指令)的方式为列表添加元素,方法调用本身就是一个相对耗时的操作。对这一点不理解也没有关系,记住“强烈建议用生成式语法来创建列表”这个结论就可以了。
- 带过滤功能的列表生成式
[exp for iter_var in iterable [if exp]]
lists = [i for i in range(10) if i < 6]
print(lists) # [0, 1, 2, 3, 4, 5]
- 循环嵌套语法格式
[exp for iter_var_A in iterable_A for iter_var_B in iterable_B]
L1 = ['香蕉', '苹果', '橙子']
L2 = ['可乐', '牛奶']
# 不使用列表生成式实现
list7 = []
for x in L1:
for y in L2:
list7.append((x, y))
# 使用列表生成式实现
list8 = [(x, y) for x in L1 for y in L2]
print(list8)
[('香蕉', '可乐'),
('香蕉', '牛奶'),
('苹果', '可乐'),
('苹果', '牛奶'),
('橙子', '可乐'),
('橙子', '牛奶')]
- 用列表生成式生成一副牌【大小王除外】
列表生成器
(exp for iter_var in iterable)
In [18]: lists = (i for i in range(10))
In [19]: lists
Out[19]: <generator object <genexpr> at 0x0000009BD7BC4E08>
In [20]: lists.__next__
Out[20]: <method-wrapper '__next__' of generator object at 0x0000009BD7BC4E08>
In [21]: lists.__next__()
Out[21]: 0
In [22]: next(lists)
Out[22]: 1
生成式和生成器的区别
先看一段代码
f = [x for x in range(1, 10)]
print(f)
f = [x + y for x in 'ABCDE' for y in '1234567']
print(f)
# 用列表的生成表达式语法创建列表容器
# 用这种语法创建列表之后元素已经准备就绪所以需要耗费较多的内存空间
f = [x ** 2 for x in range(1, 1000)]
print(sys.getsizeof(f)) # 查看对象占用内存的字节数
print(f)
# 请注意下面的代码创建的不是一个列表而是一个生成器对象
# 通过生成器可以获取到数据但它不占用额外的空间存储数据
# 每次需要数据的时候就通过内部的运算得到数据(需要花费额外的时间)
f = (x ** 2 for x in range(1, 1000))
print(sys.getsizeof(f)) # 相比生成式生成器不占用存储数据的空间
print(f)
for val in f:
print(val)
二者区别如下:
- 列表生成式: 会将所有的结果全部计算出来,把结果存放到内存中,如果列表中数据比较多,会占用过多的内存空间,可能导致MemoryError内存错误或者导致程序在运行时出现卡顿的情况。
- 列表生成器:会创建一个列表生成器对象,不会一次性的把所有结果都计算出来,如果需要序号获取数据,可以使用next()函数来获取,但要注意,一旦next()函数获取不到数据,会导致出现StopIteration异常错误,可以使用 for循环遍历生成器对象,获取所有数据。
- 列表生成式可以循环多次,列表生成器只可循环一次。
- 视情况而定,如果数据量比较大,推荐使用生成器;
简单总结
Python中的列表底层是一个可以动态扩容的数组,列表元素在内存中也是连续存储的,所以可以实现随机访问(通过一个有效的索引获取到对应的元素且操作时间与列表元素个数无关)。我们暂时不去触碰这些底层存储细节以及列表每个方法的渐近时间复杂度(执行这个方法耗费的时间跟列表元素个数的关系),等需要的时候再告诉大家。现阶段,大家只需要知道列表是容器,可以保存各种类型的数据,如何对列表数据进行增删改查就可以了。