列表和元组
创建列表
列表(list)是一种有序的多个元素的集合,是 Python 中最基本的数据结构之一。最简单的创建列表的方法是,使用方括号 [ ]
,在方括号内放置列表的元素,元素之间用逗号分隔,比如:
fruits = ["苹果", "香蕉", "桔子"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "苹果", 3.5]
我们也会经常使用 list() 函数创建列表。它可以把其它可迭代对象转换为列表,比如:
# 从元组创建列表
tup = (1, 2, 3)
list_from_tuple = list(tup) # 结果: [1, 2, 3]
# 从字符串创建字符列表
string = "hello"
list_from_string = list(string) # 结果: ['h', 'e', 'l', 'l', 'o']
这里只需要记住这个 list() 函数即可,后文还会再详细介绍可迭代对象和元组的概念。
列表推导式也是一种最常用的创建列表的方法,不过它稍微复杂一些,我们也同样留到后面再讲解。
访问列表元素
索引
与字符串的索引非常相似。列表是有序的,每个元素都有一个唯一的索引,从 0 开始计数。我们可以使用索引访问列表中的特定元素。
fruits = ["苹果", "香蕉", "桔子", "菠萝"]
print(fruits[0]) # 输出: 苹果
print(fruits[2]) # 输出: 桔子
索引数值可以是负数,负索引意味着从列表的末尾开始计数。例如,-1 是最后一个元素的索引,-2 是倒数第二个元素的索引,依此类推。
fruits = ["苹果", "香蕉", "桔子", "菠萝"]
print(fruits[-1]) # 输出: 菠萝
print(fruits[-2]) # 输出: 桔子
切片
同样与字符串类似,列表也可以做切片操作,得到列表的子集。切片操作使用冒号 :
分隔开始和结束位置。开始位置是包含的,结束位置是不包含的。如果开始位置缺失,表示从源列表最左端开始取数据;如果结束位置缺失,表示选取致源列表的最右端。
fruits = ["苹果", "香蕉", "桔子", "菠萝"]
# 获取第2到第4个元素 (索引 1, 2, 3)
print(fruits[1:4]) # 输出: ['香蕉', '桔子', '菠萝']
# 获取开始到第3个元素
print(fruits[:3]) # 输出: ['苹果', '香蕉', '桔子']
# 获取第2个元素到最后
print(fruits[1:]) # 输出: ['香蕉', '桔子', '菠萝']
在切片操作中,可以再增加一个步进值参数(第三个参数),用于指定获取元素的间隔。比如:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::2]) # 步进 2,取偶数,输出: [0, 2, 4, 6, 8]
这为我们提供了一个非常简洁的方法,把列表中的数据反向排列:只要把步进值设为 -1 即可:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::-1]) # 反向排列,输出: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
步进值虽然可以方便一些操作,但是如果同时截取列表数据的一部分,又设置步进值,可能会让操作非常复杂,难以理解。应该尽量避免这样的操作。下面是一个反例,读者在不运行代码的情况下,能推算出下面程序的运行结果吗?
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[-2:2:-3]) # 输出: ??
与索引和切片相比,Pythora 星球的居民通常使用解包操作来读取列表内的数据。
解包
解包(Unpacking),也叫拆包。它是 Python 中的一种方便的把列表中的元素“解包”(即分解)到变量中的方法。这意味着我们可以在单个操作中,将列表中的多个元素赋值给多个变量。比如:
numbers = [1, 2, 3]
a, b, c = numbers
print(a) # 1
print(b) # 2
print(c) # 3
在上面的例子中,列表 numbers 包含三个元素。通过列表解包,我们把这三个元素分别赋值给变量 a、b、c。如果只对列表中的某几个元素感兴趣,也可以部分解包:使用一元 *
运算符来表示“剩余的所有元素”:
numbers = [1, 2, 3, 4, 5]
a, b, *rest = numbers
print(a) # 1
print(b) # 2
print(rest) # [3, 4, 5]
在上面的例子中,变量 a 和 b 分别取了列表的前两个元素,而变量 rest 成为了一个包含剩余元素的新列表。在解包时,还可以忽略某些值,使用下划线 _
作为一个“丢弃”的变量。下划线 _
通常被用作占位符,表示不需要的变量或参数:
numbers = [1, 2, 3, 4, 5]
a, _, _, _, e = numbers
print(a) # 1
print(e) # 5
在这个例子中,我们只关心列表的第一个和最后一个元素,中间的元素被赋值给占位符。解包还可以应用于嵌套列表,这意味着可以直接从嵌套的列表结构中提取值。比如:
nested_list = [[1, 2], [3, 4]]
(a, b), (c, d) = nested_list
print(a, b, c, d) # 1 2 3 4
解包与索引和切片有着类似的功能,但是在可能的情况下,应该尽量使用解包,而不是索引和切片。与索引和切片相比,解包可以更清晰明确的表示需要提取哪几个元素,直接为它们赋予有意义的变量名。因此,解包可以使代码更加简洁易读。
修改列表
索引
由于列表是可变的,我们可以修改、添加或删除其中的元素。如果是修改单个的一个值,可以直接通过索引来指定要修改的元素并赋予它一个新的值:
fruits = ["苹果", "香蕉", "桔子"]
fruits[0] = "葡萄"
print(fruits) # 输出: ['葡萄', '香蕉', '桔子']
切片
类似的,我们可以通过切片来替换列表的一部分元素:
fruits = ["苹果", "香蕉", "桔子"]
fruits[1:3] = ["桃子", "蓝莓"]
print(fruits) # 输出: ['苹果', '桃子', '蓝莓']
需要注意的是,通过切片替换时,新的子列表元素的数量可以与原切片不同。这意味着我们可以用切片来插入或删除列表中的元素。
fruits = ["苹果", "香蕉", "桔子"]
# 插入元素
fruits[1:1] = ["西瓜", "芒果"]
print(fruits) # 输出: ['苹果', '西瓜', '芒果', '香蕉', '桔子']
# 删除元素
fruits[1:3] = []
print(fruits) # 输出: ['苹果', '香蕉', '桔子']
在修改列表数据的时候,尽量不要使用步进值,否则会使程序难以理解。
嵌套列表
列表里面的元素的数据类型也可以不同,比如: [1, "苹果", 3.5]
元素也可以是另一个list,比如 [1, "苹果", [5, 6, 7]]
我们经常用嵌套列表来表示矩阵、多维数组等数据结构。嵌套列表的使用索引,一层一层打开即可访问其中的数据。比如:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(matrix[1][2]) # 输出:6
matrix[1][2] = 10
print(matrix[1][2]) # 输出:10
列表的运算
连接
使用 +
运算符可以将两个列表连接(Concatenation)起来:
list1 = [1, 2, 3]
list2 = [4, 5, 6]
combined_list = list1 + list2
print(combined_list) # 输出: [1, 2, 3, 4, 5, 6]
重复
使用二元 *
运算符可以重复(Repetition)列表中的元素。
list1 = ["a", "b"]
repeated_list = list1 * 3
print(repeated_list) # 输出: ['a', 'b', 'a', 'b', 'a', 'b']
需要注意的是,*
运算符产生的多份复制是浅拷贝。所谓浅拷贝的意思是:它复制生成了新的列表,却不会复制列表内的元素。对于嵌套列表或包含其他可变数据的列表,新复制的列表中的元素依然还是指向源来列表中的元素的。比如:
elem = ["a"]
row_list = elem * 3
print(row_list) # 输出: ['a', 'a', 'a']
board_list = [row_list] * 3
print(board_list) # 输出: [['a', 'a', 'a'], ['a', 'a', 'a'], ['a', 'a', 'a']]
board_list[0][0] = 0
print(board_list) # 输出: [[0, 'a', 'a'], [0, 'a', 'a'], [0, 'a', 'a']]
当我们改变 board_list[0][0] 的值的时候,其它几个行列表中的值也被改变了。因为这些复制产生的列表实际上都是同一个列表。
检查数据是否存在
检查一个元素是否在列表中可以使用 in 关键字。这会返回一个布尔值,指示元素是否存在于列表中。这个操作也叫成员运算。
下面是一个简单的例子:
my_list = [1, 2, 3, 4, 5]
print(3 in my_list) # 输出: True
print(6 in my_list) # 输出: False
print(7 not in my_list) # 输出: True
in 关键字也可以被用在链式比较中,比如:
x = 3
my_list = [1, 2, 3, 4, 5]
print(2 < x in my_list) # 输出: True
但有时候,这样的代码很容易产生迷惑,尽量不要这样做,比如:
print(False == False in [False]) # 输出: True
上面的程序是一个链式比较操作,因此结果为 True,但不熟悉的读者可能会考虑,无论是 ==
优先级高,还是 in
优先级高,结果都应该是 False 嘛。
长度
使用函数 len() 可以获取列表的长度,也就是包含几个元素,比如:
numbers = [1, 2, 3, 4, 5, 6]
print(len(numbers)) # 输出: 6
len() 不仅可以返回字符串和列表的长度,也可以用于得到元组、字典、集合等其它一些数据类型的长度。
最大最小值
使用函数 max() 和 min() 函数可以获取列表中元素的最大最小值。比如:
numbers = [34, 12, 89, 5, 73, 23]
# 使用 max(list) 返回列表中的最大值
max_value = max(numbers)
print(f"列表中最大值是:{max_value}") # 输出: 89
# 使用 min(list) 返回列表中的最小值
min_value = min(numbers)
print(f"列表中最小值是:{min_value}") # 输出: 5
与 len() 函数类似,max() 和 min() 函数也可以被应用于字符串、元组等数据类型。
求和
sum() 函数用于计算返回列表中所有元素的总和。比如:
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
print(total) # 输出: 15
sum() 还接受一个可选的 start 参数,这个参数的值会加到总和中。默认情况下,start 的值为 0。
numbers = [1, 2, 3, 4, 5]
total = sum(numbers, 10)
print(total) # 输出: 25
常用的列表方法
与字符串类似,列表也是一种对象,也有它的方法。下面代码可以列出列表数据对象全部的属性和方法:
print(dir([]))
修改列表元素
Python 程序中,最常用的修改列表元素的方式还是利用索引和切片。不过,列表的方法也有其优势,它有方法名,可以直接看出来所做的是什么操作,程序可读性更好。经常被用来改变列表元素的列表方法包括:
- append() - 向列表末尾添加一个元素。
- extend() - 将另一个列表(或任何可迭代对象)的元素添加到当前列表的末尾。
- insert() - 在指定索引处插入一个元素。
- remove() - 删除指定的元素。
- pop() - 删除指定索引处的元素并返回它,如果不指定索引,就删除数组的最后一个元素。这个方法与 remove() 的区别在于,remove() 的输入是一个元素的值,pop() 的输入是一个元素的索引。
fruits = ["苹果", "香蕉", "桔子"]
fruits.append("草莓")
print(fruits) # 输出: ['苹果', '香蕉', '桔子', '草莓']
fruits.extend(["西瓜", "芒果"])
print(fruits) # 输出: ['苹果', '香蕉', '桔子', '草莓', '西瓜', '芒果']
fruits.insert(1, "鸭梨")
print(fruits) # 输出: ['苹果', '鸭梨', '香蕉', '桔子', '草莓', '西瓜', '芒果']
fruits.remove("西瓜")
print(fruits) # 输出: ['苹果', '鸭梨', '香蕉', '桔子', '草莓', '芒果']
removed_fruit = fruits.pop(2)
print(removed_fruit) # 输出: 香蕉
print(fruits) # 输出: ['苹果', '鸭梨', '桔子', '草莓', '芒果']
print(fruits.pop()) # 输出: '芒果'
这其中,append() 方法是最常用的,我们经常会在程序中创建一个空的列表,然后再循环结构中,不断使用 append() 把数据添加进列表。等介绍完循环语句后,我们会给出相应的示例。
排序
sort() 方法用于对列表中的元素进行排序。默认情况下,sort() 方法会按照升序对列表进行排序,但也可以接受参数来自定义排序方式。sort() 方法会修改原来的列表,也就是说,它不会创建一个新的排序后的列表,而是直接在原来的列表上进行排序。
sort() 方法接收两个参数。reverse 参数如果设置为 True,sort() 方法将进行降序排序。key 参数来指定一个函数,这个函数会在每个元素上被调用,其返回值将作为排序的依据。Python 中有一个内置的通用排序函数 sorted(),它与列表的 sort() 方法非常类似,也可以用于把列表排序,也有个 key 参数。其实上文介绍过的 max() 和 min() 函数也有个类似的 key 参数, 关于这个 key 参数的使用方法,我们将在后文介绍过相关基础知识后,在高阶函数 sorted一节一并介绍。
这里我们只看几个最基本的示例:
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
numbers.sort()
print(numbers) # 输出: [1, 1, 2, 3, 4, 5, 6, 9]
numbers.sort(reverse=True)
print(numbers) # 输出: 降序排列 [9, 6, 5, 4, 3, 2, 1, 1]
查找元素
index() 方法的名字比较迷惑,它并不是对列表做索引,而是在列表中搜索一个元素,找到返回这个元素的索引。
numbers = [34, 12, 89, 5, 12, 73, 23, 12]
print(numbers.index(12)) # 输出: 1
指定的元素可能在列表中重现了多次,但 index() 方法只会返回第一次出现的索引位置。
元素出现个数
count() 方法可以返回指定元素在列表中出现的次数,比如:
numbers = [34, 12, 89, 5, 12, 73, 23, 12]
print(numbers.count(12)) # 输出: 3
count() 只计算一个特定元素出现的次数,如果需要统计列表中每个元素出现的次数,可以参考统计次数一节的介绍。
反转列表
reverse() 方法可以反转列表中的元素。比如:
numbers = [1, 2, 3, 4, 5]
numbers.reverse()
print(numbers) # 输出: [5, 4, 3, 2, 1]
reverse() 方法的功能,与上文介绍的步进值设为 -1 的切片的功能相似。区别是,切片会生成一个新的列表,而 reverse() 方法是直接在源列表上做改动。
清空列表
clear() 方法可以移除列表中的所有元素。比如:
numbers = [1, 2, 3, 4, 5]
numbers.clear()
print(numbers) # 输出: []
复制列表
在引用型变量我们演示了这样一段程序:
a = [1, 2, 3]
b = a
b[0] = 5
print(a) # 输出: [5, 2, 3]
使用赋值语句 b = a
会让 b 和 a 指向同一个列表,改变其中一个变量指向的数据,另一个变量指向的数据也能看到同样的变化,因为它们是同一个数据嘛。但有时候,我们希望两个变量可以分别变化,那么就不能直接使用赋值语句了,而是可以使用 copy() 方法把列表复制一份,再赋值给新的变量:
original_list = [1, 2, 3, 4, 5]
copied_list = original_list.copy()
# 修改原列表不会影响复制的列表
original_list[4] = '**'
print(original_list) # 输出: [1, 2, 3, 4, '**']
print(copied_list) # 输出: [1, 2, 3, 4, 5]
需要注意的是,copy() 方法也只是浅拷贝。所谓浅拷贝的意思是:copy() 方法虽然会生成新的列表,但它不会复制列表内的元素。对于嵌套列表或包含其他可变数据的列表,新复制的列表中的元素依然还是指向源来列表中的元素的。比如:
original_list = [[1, 2], [3, 4, 5]]
copied_list = original_list.copy()
# 对于嵌套列表,修改原列表中数据依然会影响复制的列表
original_list[1][2] = '**'
print(original_list) # 输出: [[1, 2], [3, 4, '**']]
print(copied_list) # 输出: [[1, 2], [3, 4, '**']]
对于嵌套列表,如果想让两个列表彻底分开,必须要深拷贝才行。这需要使用 copy 模块中的 deepcopy() 函数来创建一个深拷贝,它会递归地拷贝列表所有的内部元素:
import copy
original_list = [[1, 2], [3, 4, 5]]
copied_list = copy.deepcopy(original_list)
# 深拷贝可以保证修改原列表不会影响复制的列表
original_list[1][2] = '**'
print(original_list) # 输出: [[1, 2], [3, 4, '**']]
print(copied_list) # 输出: [[1, 2], [3, 4, 5]]
无比灵活的 Python
细心的读者肯定已经发现了,我们上面介绍的内容中,有很多重复的功能。比如,需要从列表中删除一个元素,有很多办法都可以:
- 使用 del 语句
my_list = ['a', 'b', 'c', 'd']
del my_list[1] # 删除索引为 1 的元素 'b'
print(my_list) # 输出: ['a', 'c', 'd']
- 使用 pop() 方法
my_list = ['a', 'b', 'c', 'd']
my_list.pop(1) # 删除索引为 1 的元素 'b'
print(my_list) # 输出: ['a', 'c', 'd']
- 使用 remove() 方法
my_list = ['a', 'b', 'c', 'd']
my_list.remove('b') # 删除索引为 1 的元素 'b'
print(my_list) # 输出: ['a', 'c', 'd']
- 使用切片
my_list = ['a', 'b', 'c', 'd']
my_list[1:2] = [] # 删除索引为 1 的元素 'b'
print(my_list) # 输出: ['a', 'c', 'd']
除了上面提到的这几种方法,我们还将在后文探讨其它一些可以删除列表元素的技巧,比如列表推导式、filter() 函数等。不仅仅是列表删除元素功能,实际上,对于大多数任务来说,Python 都有多种解决方案可供选择。在本书的后续章节中,会经常发现我们使用不同的方法实现了许多相似的功能。这正体现了 Python 的灵活性。在选择最佳解决方案时,我们需要考虑每种方案的微妙差异,包括功能、性能、代码简洁性、一致性、可读性,以及符合公司或组织的标准和合作者对代码的理解程度等因素,从而做出恰当的选择,以应对当前的具体问题。
元组
元组(Tuple)和列表非常相似,它们都是有序的集合,即元素的顺序是固定的,不会随机变动。它们的元素类型,它们的很多基本操作也都是相同的。
从外观上看,元组与列表的唯一区别在于,元组使用小括号表示,列表使用大括号表示。这只是表面的,本质上最主要的区别在于,列表是可变的,我们可以修改、添加或删除列表中的元素;而元组是不可变的,一旦创建,就不能再修改、添加或删除元组中的任何元素了。由于元组不可变,它的运算和功能会少一些,比如元组不支持 append() 这类用来改变元素的方法,只支持读取数据的方法。由于,元组不可变,它更加安全,更加节省内存,访问速度也更快。当需要创建一个不变的列表时,我们应该使用元组。
Python 会把用逗号分隔的几个数据自动打包成元组:
my_tuple = 3, 5, 7
print(my_tuple) # 输出: (3, 5, 7)
一个容易出现的错误是使用数据时候,后面带了个逗号,结果 Python 不会保存,而是会把它自动被打包成元组:
a = 3,
print(a) # 输出: (3,)
读取元组中数据的操作与读取列表元素的操作是完全相同的,我们同样可以使用索引、切片、解包等操作读取元组中的数据,比如:
my_tuple = (3, 5, 7)
a, b, c = my_tuple
print(a) # 输出: 3
print(b) # 输出: 5
print(c) # 输出: 7
first, *middle, last = (1, 2, 3, 4, 5)
print(first) # 输出: 1
print(middle) # 输出: [2, 3, 4] ** 注意,这部分数据变成了列表而不是元组
print(last) # 输出: 5
因为 Python 会把用逗号分隔的几个数据自动打包成元组,所以有些赋值操作看起来有问题,其实却是可行的,比如,下面这段程序与上面的示例功能完全相同,只是省略了表示元组的小括号:
first, *middle, last = 1, 2, 3, 4, 5
print(first) # 输出: 1
print(middle) # 输出: [2, 3, 4] ** 注意,这部分数据变成了列表而不是元组
print(last) # 输出: 5
其实字符串也可以使用同样的拆包操作,比如:
s = "abc"
m, n, o = s
print(f"拆包后的字符是: {m}, {n}, {o}") # 字符串拆包后,成为单独的字符
需要注意的是,元组的元素不能被改变。但如果元组的元素本身是可变数据类型,那么这个数据本身是可能会被改变的,比如:
a = ([1,2],[3,4])
a[1].append(5)
print(a) # 输出: ([1, 2], [3, 4, 5])
Python 中还有一个与元组类似的数据类型:命名元组,它可以给元组中每个元素都起一个命名,我们将在后文详细介绍。
练习
- 反转列表:把编写程序,把一个列表中的数据反向排列
- 拆分列表:把一个整数列表,按照其元素的奇偶性,拆分成两个列表分别保存奇数和偶数。
- 次大数据:在一个实数列表中,找出第二大的数。
- 拆分列表:输入一个列表和一个整数 n,将列表按每 n 个元素分为一个子列表,总共生成 n 个新的列表。
- 循环移位:输入一个列表和一个正整数 n,将列表中的元素循环右移 n 位。