06-列表
1. 列表结构
在前面的初识数据类型中,已经讲解了列表的基本创建,接下来我们来看看列表创建的一些注意点。
在前面的数据类型介绍中,我们已经学习了列表的基本创建方式,接下来让我们进一步了解列表创建时需要注意的细节。
1.1 列表的基本语法
利用 中括号 表示列表使用 中括号 [] 表示列表;- 列表内的元素用 逗号, 隔开;
注意是 英文输入法 下的逗号请确保使用 英文输入法 下的逗号,以避免语法错误。
我们创建两个基础列表试试吧,代码如下:
student1 = ['aiyuechuang', 18, 'class01', 202405]
student2 = ['bornforthis', 19, 'class02', 202402]
1.2 列表的三大特性
别忘记,列表的三大特性:有序性,任意数据类型,可变性。对于列表的 可变性,我们指的是:可以修改列表里的内容。(后面会讲解到)
在使用列表时,需要牢记列表的三大特性:
- 有序性:列表中的元素是按照一定的顺序存储的,可以通过索引访问。
- 支持任意数据类型:列表可以存储不同类型的元素,如字符串、整数、浮点数甚至其他列表。
- 可变性:列表中的元素是可以被修改的,后续章节将详细介绍这一特性。
1.3 字符串与列表的强制转换
在前面学习字符串时,我们探讨了强制转换的概念,并分析了哪些数据类型可以成功转换,哪些会导致错误。尽管强制转换有时会带来问题,但在某些情况下,我们可以利用这些特性来实现特殊的功能,来达到意想不到的功能。
在前面的字符串中,我们探讨了强制转换。以及哪些适合强制转换,哪些强制转换会出现问题,以及强制转换出现报错!虽然强制转换会出现问题,但有时也可以利用这样的特点(问题)达到意想不到的功能。
例如:我们在字符串的强制转换中发现,会把字符串每个字符拆解出来并返回列表,我把代码放在下面:
例如,将字符串强制转换为列表时,每个字符都会被拆分成单独的元素,示例如下:
In [1]: user_input = input(":>>>")
:>>>[3, 22, 1997]
In [2]: user_input
Out[2]: '[3, 22, 1997]'
In [3]: list(user_input)
Out[3]: ['[', '3', ',', ' ', '2', '2', ',', ' ', '1', '9', '9', '7', ']']
从上面的代码输出结果就可以很直观的,字符串 '[3, 22, 1997]'
每个字符都被拆解出来了。利用上面的特点,我们可以把字符串的内容强制转换成列表,得到每个字符所组成的列表。
从上面的示例可以看出,字符串 '[3, 22, 1997]'
在转换为列表后,每个字符都被拆分成单独的元素。
我们可以利用这个特性,将字符串内容强制转换成列表,得到包含各个字符的列表。例如:
string_to_list = list('Bornforthis.cn')
print(string_to_list)
# ---output---
['B', 'o', 'r', 'n', 'f', 'o', 'r', 't', 'h', 'i', 's', '.', 'c', 'n']
通过这样的转换方式,我们可以轻松地将字符串的每个字符拆解出来,从而对字符串进行更灵活的操作。
2. 获取列表中的某个元素
前面我们学习了字符串的元素获取,接下来我们来看看列表的元素获取,在 Python 当中字符串的元素获取整体结构是类似的。
在前面,我们学习了如何获取字符串中的元素。现在,让我们看看列表的元素获取方式,它与字符串的索引访问方式非常相似。
2.1 列表下标的组成
编程语言中通常 第一个位置的编号是 0。
下图展示的是 grade 列表中每个元素对应的下标(索引):

2.2 提取单个元素
中括号内数字指定元素位置。
使用索引值可以获取列表中的单个元素,索引值写在中括号 []
内。(索引值就是下标)
grade = [98, 99, 95, 80]
print(grade[0]) # 98
print(grade[0] + grade[3]) # 178
2.3 获取列表中连续的几个元素

中括号内用 起始位置:结束位置 描述- 通过 切片语法 起始位置:结束位置 获取部分元素。
注意: 不包括结束位置的元素。- 注意:切片操作不包含结束索引对应的元素。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:6]) # [2, 3, 4, 5]
2.4 获取列表中连续且特定间隔的元素

更细致的用法 起始位置:结束位置:步长- 通过 切片语法 起始位置:结束位置:步长 实现步长控制。
注意: 不包括结束位置的那个元素- 注意:步长控制时,依然不包含结束索引对应的元素。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[1:7:2]) # [1, 3, 5]
通过索引和切片操作,我们可以灵活地访问和提取列表中的元素,以满足不同的需求。
3. 列表的切片赋值
3.1 深入理解列表的切片赋值机制
编写下面的代码并思考最后输出的结果:
让我们先看下面这段代码,并思考其最终的输出结果:
name = list('Python')
print(name)
name[2:] = list('abc')
print(name)
如果你运行它,会发现最终打印出的结果可能跟你的直觉不太一致。接下来,我们一步步在 iPython 中解析这段代码,来探究背后的原理。
运行这段代码后,你可能会觉得输出结果有些出乎意料。为了更好地理解其中的原因,我们通过 iPython 环境一步步深入解析,揭开背后的原理。
上面的代码结果,是不是有些疑惑?我们来一步步使用 iPython 解析一下,看看其中是什么原理。
首先我们先创建 name 变量,存储字符串 'Python'
并使用 list()
函数强制转换成列表。
3.1.1 步骤一:创建并查看列表
首先,创建变量 name
并将字符串 'Python'
通过 list()
函数强制转换成列表后,赋值给变量 lst
:
代码如下:
In [1]: name = 'Python'
In [2]: lst = list(name)
In [3]: lst
Out[3]: ['P', 'y', 't', 'h', 'o', 'n']
上面的代码输出想必都没什么问题,我们接下来继续提取列表中的 ['t', 'h', 'o', 'n']
,可以直接使用如下代码实现:
到目前为止,列表的输出符合我们的预期,每个字符都已成功变成列表元素。
3.1.2 步骤二:使用切片获取子列表
这一步应该在意料之中:字符串每个字符都变成了列表中的一个元素。接着,我们来查看切片 lst[2:]
:
接下来,我们使用切片语法提取列表 lst
中索引为 2 及之后的所有元素:
In [4]: lst[2:]
Out[4]: ['t', 'h', 'o', 'n']
正如我们的预期,得到的结果清晰明确,列表切片 [2:]
确实返回了我们想要的元素片段。
正如预期,切片 lst[2:]
获得从索引 2 开始到列表末尾的所有元素,即 ['t', 'h', 'o', 'n']
。
3.1.3 步骤三:将字符串转换为列表
我们对字符串 'abc'
也做一次 list()
转换:
在下一步操作中,我们要将字符串 'abc'
也转换为列表,以便后续赋值操作:
接下来,我们需要强制字符串 'abc'
便于下一步操作:
In [5]: list('abc')
Out[5]: ['a', 'b', 'c']
转换后的列表结构同样非常明确。
3.1.4 步骤四:关键的切片赋值操作
现在,我们将新创建的列表 ['a', 'b', 'c']
赋值给 lst[2:]
,这里的切片赋值操作是关键:
重点来了,我们使用 list('abc')
赋值给 lst[2:]
,而 lst[2:]
又代表 lst 列表中的 ['t', 'h', 'o', 'n']
,所以实际覆盖的是 lst 列表,最终输出:['P', 'y', 'a', 'b', 'c']
关键的操作在这里:将新创建的 list('abc')
赋值给 lst[2:]
。要注意的是,lst[2:]
代表的是原列表从索引 2 开始的位置,也就是 ['t', 'h', 'o', 'n']
,因此这次赋值会直接“覆盖”原列表相应位置上的元素。代码如下:
In [6]: lst[2:] = list('abc')
In [7]: lst
Out[7]: ['P', 'y', 'a', 'b', 'c']
最终的输出是 ['P', 'y', 'a', 'b', 'c']
,而不是许多人可能最初期待的 ['P', 'y', 't', 'h', 'o', 'n', 'a', 'b', 'c']
。这是因为切片赋值会替换指定范围的所有元素,而不仅仅是插入新元素。
这里发生了什么?实际上,切片赋值并非仅仅插入元素,而是直接用新序列替换了原列表中对应切片位置的所有元素。因此,原先从索引 2 开始的 ['t', 'h', 'o', 'n']
被新列表 ['a', 'b', 'c']
完全取代,并且列表的长度也随之自动调整。
通过上面的讲解,你可以清晰地观察到列表切片在 Python 中的工作方式:当你对一个切片进行赋值时,新的序列会替换旧的切片区域,而列表长度也会根据新序列自动调整。
讲了这么多,不如一图胜千言。下面我也给你做了一张图,你可以直接查看:

3.2 列表切片赋值进阶:插入与删除元素
在理解了前面的基础操作之后,我们继续来看一个稍复杂一点的示例代码,深入体会列表切片赋值的妙用:
numbers = [1, 5]
numbers[1:1] = [2, 3, 4]
print(numbers)
numbers[1:4] = []
print(numbers)
你能够准确预测出它的输出吗?让我们再一次通过 iPython 环境逐步分析其中的细节。
3.2.1 步骤一:理解 [1:1]
切片代表什么?
我们首先定义一个列表变量 numbers
:
In [8]: numbers = [1, 5]
接下来,查看一下切片 [1:1]
到底代表什么内容:
In [9]: numbers[1:1]
Out[9]: []
可以看到,numbers[1:1]
是一个空列表。原因很简单:它表示从索引 1 开始,到(但不包括)索引 1 结束的元素区间,因此自然不包含任何元素。
上面的讲解还是过于简洁,我们来详细讲解一下:numbers[1:1]
其中第一个 1,也就是冒号前面的 1(代表起始位置),要取到列表元素 5。而其中第二 1,也就是冒号后面的 1(代表结束位置)结束位置代表不提取该位置列表的元素 5,而是要提取该结束位置之前的元素。
说到这,有没有发现对于相同索引(下标)位置开始、结束。此时这种情况像拔河一般,开始位置和结束位置互不相让,开始位置要取到5,而结束位置不让取到5且想要取到5之前的元素,但是5之前的元素,因为开始索引,也取不到,最终指向的是元素 1和5之间。而1和5元素之间,没有其它元素,最终为空,也就是得到空列表。
上面的讲解还是过于简洁,我们来详细分析一下。
首先,我们明确一下切片 [1:1]
的含义:
- 第一个
1
(开始位置)表示从索引 1 的元素开始提取,即元素5
。 - 第二个
1
(结束位置)表示不提取索引 1 的元素,而是只取结束位置之前的元素。
注意此处的特殊性: 当起始位置和结束位置相同时,情况变得有趣起来。
- 特殊性一: 开始位置希望提取元素
5
,而结束位置却限制不能提取元素5
。 - 特殊性二: 结束位置要提取结束位置之前的元素,但被开始位置限制而不能被提取。
因此二者形成了一种对峙(类似拔河比赛,互不相让),最终使得切片位于元素 1
与元素 5
之间。但由于这两个元素之间不存在其他元素,最终切片为空,即得到空列表。
3.2.2 步骤二:利用切片赋值插入元素
既然 [1:1]
是空的,将元素赋值给空切片会发生什么呢?我们继续实验:
In [10]: numbers[1:1] = [2, 3, 4]
In [11]: numbers
Out[11]: [1, 2, 3, 4, 5]
结果清晰明了,赋值给空切片 [1:1]
实际上是在列表索引为 1 的位置插入了新的元素 [2, 3, 4]
,实现了“插入”效果。
通过切片赋值的方式,我们不仅能够替换列表中已有的元素,也能够轻松地在任意位置插入新的元素。
小记: 当我们对空切片赋值时,实际效果便是在此位置插入新元素。
3.2.3 步骤三:利用切片赋值删除元素
现在我们再来看如何利用切片赋值来删除列表元素。如果我们给一个切片赋值为空列表 []
,又会怎样呢?
In [12]: numbers[1:4] = []
In [13]: numbers
Out[13]: [1, 5]
你会发现,原本索引为 1 到 3 的元素 [2, 3, 4]
被删除了。通过这种方式,我们实现了对列表中多个元素的批量删除。
总结一下:
- 将元素赋值给空切片可以实现插入元素的效果;
- 将空列表赋值给非空切片可以实现对列表元素的批量删除。
理解这些细节之后,你就能更加自如地掌握 Python 列表中切片赋值的高级技巧了。
4. 小试牛刀
获取用户输入两个值,一个是要插入的位置,一个是要插入这个位置的值。
在某些场合,我们需要根据用户输入的“位置”和“值”将一个新元素插入到列表当中。
举个例子,假设有一个列表 numbers = [1, 2, 3, 5, 6]
,如果用户输入的插入位置为 3
,待插入的值为 4
,最终我们想获得的结果是 [1, 2, 3, 4, 5, 6]
。值得注意的是,这里我们不使用 Python 内置的 .insert()
方法,而是通过列表切片手动完成插入操作。
现在我给你一个具体的示例,给定下面列表:
numbers = [1, 2, 3, 5, 6]
程序运行后:
Enter position: 3
Enter value: 4
[1, 2, 3, 4, 5, 6]
现在,你可以开始思考了。结合上面所学的列表切片赋值,看自己能否一举实现。
如果你思考了一段时间后,还是没有思路,可以看看我下面给你提供的思路。
关键思路:
- 先读取用户输入的 插入位置
position
和 插入值value
。 - 使用 Python 的切片机制,将列表分为两部分:
numbers[:pos]
(从头到pos-1
下标)和numbers[pos:]
(从pos
下标到结尾)。 - 将新元素
[val]
拼接进中间位置,从而生成新的列表。
代码实现如下:
numbers = [1, 2, 3, 5, 6]
position = int(input('Enter position: '))
value = int(input('Enter value: '))
result = numbers[:position] + [value] + numbers[position:]
print(result)
另一种方法实现如下,可以自行思考:
numbers = [1, 2, 3, 5, 6]
position = int(input('Enter position: '))
value = int(input('Enter value: '))
numbers[position: position] = [value]
print(numbers)
5. 在列表的特定位置插入元素「.insert(index, element)
」
前面我们实现了,不使用 insert()
来实现插入数据。接下来,我们来具体讲解一下 insert()
。
.insert(index, element)
是一个列表的基本方法,用于在列表的指定位置插入一个元素。
它的基本语法是:
list.insert(index, element)
index
: 指定要插入元素的位置。索引从 0 开始。如果指定的索引超出了列表的当前长度「不会报错」,则元素将被添加到列表的末尾。element
: 这是你想要插入列表的元素。
假如,我们要在列表的 3 号位上,插入数字 4,我们的代码实现如下:
numbers = [1, 2, 3, 5, 6]
numbers.insert(3, 4)
print(numbers) # [1, 2, 3, 4, 5, 6]
6. 列表长度
在 Python 中,列表是一种可变的数据结构。要获取列表中的元素个数,可以使用内置函数 len()
。例如:
获取列表长度,使用 len()
:
student_list = ['李雷', '韩梅梅', '马冬梅', 'AI悦创', '黄婉棠']
print(len(student_list))
# ---output---
5
上述代码会输出 5,因为 student_list
中共有五个元素。
7. 修改列表中的元素
列表中的元素是可以被修改的。修改操作可以针对单个元素,也可以使用切片一次性修改多个连续的元素。修改列表中元素的步骤:
- 步骤一: 找到需要修改的元素编号(下标、索引都是一个意思);
- 步骤二: 开始修改
列表名[编号] = 新值
;
一图胜千言,我还是准备了一张图便于你理解:

下面我们分别来看这几种情况。
7.1 修改单个元素
通过索引定位单个元素,然后直接赋值新的内容即可。
name = ['lilei', 'hanmeimei']
print('before:', name)
name[0] = 'madongmei'
print('after:', name)
# ---output---
before: ['lilei', 'hanmeimei']
after: ['madongmei', 'hanmeimei']
在这个例子中,列表 name
中第一个元素 'lilei'
被替换成了 'madongmei'
。
多个元素修改
7.2 修改多个元素
使用切片赋值可以同时修改多个元素。切片赋值有多种情况,下面分别说明:
7.2.1 替换元素数量相同
@tab 修改元素数量一样
当切片替换的新序列长度与原来切片的长度一致时,列表长度保持不变。例如:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print('before:', numbers)
numbers[1:5] = ['one', 'two', 'three', 'four']
print('after:', numbers)
# ---output---
before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
after: [0, 'one', 'two', 'three', 'four', 5, 6, 7, 8, 9, 10]
7.2.2 替换元素数量不相同
@tab 修改改元素数量不一样
切片赋值时,新序列的元素数量可以与原来切片的元素数量不同,此时列表的长度会相应改变。例如:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print('before:', numbers)
# 替换区间 [1:5] 的元素: [1, 2, 3, 4],用两个新元素替换原来的四个元素
numbers[1:5] = ['one', 'two']
print('after:', numbers)
# ---output---
before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
after: [0, 'one', 'two', 5, 6, 7, 8, 9, 10]
7.2.3 使用字符串进行赋值
多个修改的对象也可以是字符串,当赋值对象为字符串时,Python 会将字符串拆分为单个字符序列进行赋值。示例如下:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print('before:', numbers)
# 元素数量不一样 & 字符串自动拆开成列表
# 使用字符串 'bornforthis' 进行赋值,字符串自动拆分成字符列表
numbers[1:5] = 'bornforthis'
print('after:', numbers)
# ---output---
before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
after: [0, 'b', 'o', 'r', 'n', 'f', 'o', 'r', 't', 'h', 'i', 's', 5, 6, 7, 8, 9, 10]
7.2.4 使用其他数据类型进行赋值
这部分,在我开始开始讲解之前。请先自行编写代码测试,并得出属于你自己的结论,找到哪些数据类型是可以进行赋值,哪些数据类型是会报错或者其中不报错的,有何特点。然后,再带着你自己的理解来阅读本章节后序内容。
在切片赋值中,除了列表和字符串,还可以使用元组、集合和字典(仅取其键)作为赋值对象,但要注意各自的特点:
字典赋值
当用字典进行赋值时,实际会使用字典中的键进行替换:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print('before:', numbers) numbers[1:5] = {'a': 1, 'b': 8} print('after:', numbers) # ---output--- before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] after: [0, 'a', 'b', 5, 6, 7, 8, 9, 10]
集合赋值
由于集合是无序的,所以使用集合进行赋值时,元素的顺序可能与原来不一致:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print('before:', numbers) numbers[1:3] = {'aiyc', 'look', 'pool'} print('after:', numbers) # ---output--- before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] after: [0, 'pool', 'look', 'aiyc', 3, 4, 5, 6, 7, 8, 9, 10]
注意:在多个元素修改的场景中,赋值的对象必须是可迭代对象。布尔值(如 True
或 False
)不是可迭代的,因此不能用于切片赋值,否则会产生 TypeError
异常。例如:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print('before:', numbers)
# 尝试用布尔值进行切片赋值,结果会报错
numbers[1:5] = True
print('after:', numbers)
# ---output---
Traceback (most recent call last):
File "/Users/huangjiabao/demo.py", line 5, in <module>
numbers[1:5] = True
~~~~~~~^^^^^
TypeError: can only assign an iterable
before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
通过上述示例,我们可以看到列表的修改非常灵活。根据具体需求选择不同的赋值方式,可以使代码更加简洁且功能更强大。
老样子,总结一下:
多个元素修改情况下,可以使用的对象:可用于切片赋值的对象(必须是可迭代的):
- 列表
- 元组
集合「不按集合原本的顺序」- 集合(元素顺序不固定)
- 字符串(按字符拆分)
- 字典(只取字典中的键 key)
多个元素修改情况下,不可以的对象:不可用于切片赋值的对象(非可迭代对象):
- 布尔型
- 数字型
切片赋值要求右侧的对象必须是可迭代的,因此布尔值和数字会导致错误。
8. 向列表添加元素
Python 提供了多种方法向列表中添加元素,常见的方法有 .append()
和 .extend()
,它们的功能有所不同,使用时需要注意区别。
在 Python 中,最常见的向列表添加元素的方法有 append()
和 extend()
。它们都可以在已有的列表中插入新的内容,但使用场景和效果稍有不同。
在 Python 中,我们经常需要向列表中添加新的元素,可以使用多种方法来实现,下面分别介绍常用的两种方法:
8.1 添加单个元素——append()
在列表变量后使用 .append(要添加的元素)
,即可在列表末尾添加单个元素。其基本语法如下:
使用 append()
方法,可以向列表的末尾添加单个元素。其基本语法如下:
如果只是需要在列表末尾添加一个元素,可以使用 append()
方法。它的用法非常简单:
列表.append(要添加的元素)
示例:
inventory = ['钥匙', '毒药']
print('before:', inventory)
inventory.append('解药') # 在列表末尾添加"解药"
print('after:', inventory)
# ---output---
before: ['钥匙', '毒药']
after: ['钥匙', '毒药', '解药']
需要注意的是上面虽然实现了,向列表添加元素。但 append()
方法每次只能添加一个元素。如果尝试使用 append()
传入多个元素(如一个列表),整个列表会作为一个单独的元素添加进原列表中。
需要注意的是,.append()
每次只能添加单个元素。如果我们尝试一次性添加多个元素,可能会得到意想不到的结果。例如:
上面虽然实现了,向列表添加元素,但局限性也很明显,每次只能添加一个元素,不能多个元素。有可能你会想如下方式添加:
inventory = ['钥匙', '毒药']
print('before:', inventory)
inventory.append(['解药', '感冒药', '抗生素'])
print('after:', inventory)
那在你运行之后,可知会把整个列表放进去。
此时,['解药', '感冒药', '抗生素']
作为一个整体(即嵌套列表)被加入 inventory
中,而不是分别添加多个元素。输出结果如下:
before: ['钥匙', '毒药']
after: ['钥匙', '毒药', ['解药', '感冒药', '抗生素']]
其它数据类型,你可以自行测试一下。例如:元组、字典、字符串等数据类型。要随时学会举一反三,这样才是有效学习。
因为 append()
只会把你传进去的“那个东西”当作“一个整体”来添加,所以这里最终在列表中放进了一个子列表。而我们需要一次性“拆开”加入多个元素时,就应该使用接下来的 extend()
方法。
不过,这里我还是给你演示一下,一个一个添加多个元素的代码,毕竟没有对比就没有伤害:
inventory = ['钥匙', '毒药']
inventory.append('解药')
inventory.append('感冒药')
inventory.append('抗生素')
print(inventory)
# ---output---
['钥匙', '毒药', '解药', '感冒药', '抗生素']
8.2 添加多个元素——extend()
如果想一次性添加多个元素,除非使用 .append()
一个一个手动的添加进去。直接使用则会形成嵌套列表,此时方便的方法就是使用 extend()
方法,接下来我们来看此方法。
extend()
方法用于将多个元素逐个添加到列表中,而不是作为一个整体加入。其基本语法如下:
extend()
方法可以把传进去的可迭代对象(如列表)中的所有元素逐个放入到目标列表中。这样就不用循环手动一个一个地 append()
,语法如下:
列表.extend(可迭代对象)
示例:
inventory = ['钥匙', '毒药', '解药']
inventory.extend(['迷药', '感冒药'])
print(inventory)
# ---output---
['钥匙', '毒药', '解药', '迷药', '感冒药']
因此,在需要一次性添加多个元素的时候,extend()
往往更方便。
总结:
如果比喻吃饭,.append()
函数犹如狼吞虎咽的壮汉,一口吞下一整只烤鸡。而 .extend()
做到吃鸡的时候,每口细嚼慢咽。
append(x)
: 整体添加一个元素x
(即便x
是一个列表,也会作为单个元素)。extend(iterable)
: 逐个添加iterable
中的所有元素。
就如同 append 毫不克制的一次性添加一个元素,不支持逐个添加。
9. 删除列表中的元素
Python 提供了多种删除列表元素的方法,分别适用于不同的情况。
Python 提供了多种删除列表元素的方法,比如 del
、pop()
、remove()
等。根据不同场景,可以自由选择最合适的方法。
9.1 使用 del
del
需要指定列表中要删除的单个元素或多个元素,如果不指定元素,则删除整个变量(列表)。
del
关键字可以删除变量本身,也可以删除列表中的一个或多个元素。使用时需要指定要删除的变量或列表中元素的索引位置。如果什么都不指定,直接写 del 列表变量
,就会把整个列表变量从内存中移除,后续再使用这个变量就会导致错误。
del
语句可以用于删除列表中指定索引的元素,也可以删除整个列表。
- 删除指定索引的元素
student_list = ['李雷', '韩梅梅', '马冬梅']
del student_list[0] # 删除索引 0 处的元素("李雷")
print(student_list)
# ---output---
['韩梅梅', '马冬梅']
- 删除整个列表
如果不指定删除的元素,则删除整个变量。
如果不指定索引,而是直接删除列表变量,则整个列表都会被删除:
student_list = ['李雷', '韩梅梅', '马冬梅']
del student_list # 删除整个列表
print(student_list) # 访问已删除的变量会报错
# ---output---
Traceback (most recent call last):
File "/Users/huangjiabao/demo.py", line 3, in <module>
print(student_list)
NameError: name 'student_list' is not defined
可以看到,student_list
这个变量在执行完 del student_list
后就不再存在了。
9.2 使用 pop()
pop()
函数默认删除列表中的最后一个元素,也可以传参数指定要删除元素的下标,删除时会返回删除的元素。
pop()
方法用于删除并返回指定索引的元素。如果不指定索引,则默认删除最后一个元素。
student_list = ['李雷', '韩梅梅', '马冬梅']
# 删除并返回最后一个元素
removed = student_list.pop() # 不传参则删除列表末尾元素
print('删除的元素:', removed)
print(student_list)
# 删除索引 0 处的元素
student_list.pop(0) # 传参 0 则删除第 0 号位置的元素
print(student_list)
# ---output---
删除的元素: 马冬梅
['李雷', '韩梅梅']
['韩梅梅']
值得注意的是,pop()
方法可以将被删除的元素作为返回值用在其他地方,有时在处理数据时非常方便。
上面我提到有时在数据处理是非常方便,这里我稍微做个扩展,如果你一时无法理解也没事。后期学完本书后,再回来阅读此部分也不迟。
模拟堆栈或队列
在实现“先进后出”(LIFO)的堆栈结构时,常用
append()
入栈,pop()
出栈。利用pop()
的返回值,可以直接得到最后压入栈的那个元素并进行处理。例如:stack = [] # 入栈 stack.append('task1') stack.append('task2') # 出栈并获取元素 current_task = stack.pop() print(current_task) # 'task2'
这样就能第一时间拿到出栈的任务,进行后续操作(如打印、处理等)。
有序处理列表中的元素
比如你有一个待办事项列表(To-Do List),想在列表的末尾添加最新的任务,并从末尾弹出“最紧急”或“最近添加”的那个任务去做。此时
pop()
返回的就是刚移除的任务项,你可以立即根据这个返回值来执行对应的逻辑。todo_list = ['写报告', '复习资料', '预约会议'] current_task = todo_list.pop() # 删除并获取 '预约会议' print(f"正在处理任务:{current_task}")
从特定位置删除并获取元素
如果给
pop()
传入一个索引,它就会删除并返回该位置的元素。对于存储了结构化数据或需要在特定位置“取走一个元素”再做分析的情形,会非常方便。students = ['Alice', 'Bob', 'Charlie', 'David'] # 比如要把队列里的第一个人 pop 出来做一些操作 first_student = students.pop(0) print(first_student) # 'Alice'
在中途需要“转移”元素的情况
有时需要把某些元素从一个列表取走,再“放”到另一个列表里,这里也会用到
pop()
的返回值。例如,先用pop()
移除一个待处理对象,然后把它添加到“已处理”的列表中去。to_process = ['文件A', '文件B', '文件C'] processed = [] while to_process: file = to_process.pop() # 获取并删除列表最后一个 # 对 file 做一些处理操作 processed.append(file) print(processed) # ['文件C', '文件B', '文件A'] (后弹先处理的顺序)
总之,pop()
方便的地方就在于**“立刻拿到被删除的元素”**这一点,可以让我们在删除的一瞬间对该元素做一系列后续处理,而不用再通过其他方式去查找或记录那个元素。
9.3 使用 remove()
remove()
指定删除列表中某个元素,
如果你需要根据元素的内容来删除元素,而不想费心去查找索引,就可以使用 remove()
。它会删除列表中第一个匹配的元素,例如:remove('aiyc')
则指定删除列表中的 'aiyc'
元素。如果元素不存在,则会报错。
remove()
方法用于按值删除列表中的元素,如果元素不存在,则会报错。
student_list = ['李雷', '韩梅梅', '马冬梅']
student_list.remove('韩梅梅')
print(student_list)
# ---output---
['李雷', '马冬梅']
需要注意的是,如果列表中没有匹配到指定的值,就会抛出 ValueError
异常。
student_list = ['李雷', '韩梅梅', '马冬梅']
student_list.remove('小悦')
print(student_list)
# ---output---
Traceback (most recent call last):
File "/Users/huangjiabao/demo.py", line 2, in <module>
student_list.remove('小悦')
ValueError: list.remove(x): x not in list
10. 两个列表相加 连接两个列表
可以使用 +
运算符直接合并两个列表:
两个列表可以使用 +
号进行拼接,得到一个新的列表对象。例如:
直接使用加号加就可以。
numbers1 = [0, 1, 2, 3, 4]
numbers2 = [5, 6, 7, 8, 9]
print(numbers1 + numbers2)
# ---output---
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
此操作不会影响原列表 numbers1
和 numbers2
,而是返回一个新的列表。
11. 判断某个元素是否存在于列表中「Value in Sequence」
在 Python 中,可以使用 in
关键字来判断某个值是否存在于列表(或其他序列)中,结果会返回 True
或 False
:
如果想要判断某个元素是否存在列表当中,可以使用 in
关键词来判断某个值是否存在于列表,格式:Value in Sequence
,这个方法适用于其它序列,结果会返回 True
或 False
。
一图胜千言,看下图表达(不用在意 if 判断,后续章节会讲):

可以使用 in
关键字判断某个元素是否存在于列表中:
inventory = ['钥匙', '毒药', '解药']
print('解药' in inventory)
print('迷药' in inventory)
# ---output---
True
False
12. 获取列表中某个元素重复的次数——.count()
如果想知道某个元素在列表中出现了多少次,可以使用 count()
方法:count()
方法可以统计某个元素在列表中出现的次数:
numbers = [0, 1, 1, 2, 3, 4, 1]
print(numbers.count(1))
# ---output---
3
说明 1
在该列表中一共出现了 3 次。
13. 获取列表中某个元素第一次出现的位置——index()
用 列表.index(元素) 来获取,如果元素不存在则会报错。
index()
方法返回某个元素在列表中第一次出现的索引,语法:列表.index(元素),如果元素不存在,则会报错:
numbers = [0, 1, 1, 2, 3, 4, 1]
print(numbers.index(1)) # 1
14. 列表排序
Python 提供了多种方式对列表进行排序,常见的有 list.sort()
和内置函数 sorted()
。二者最大的区别在于:
list.sort()
:原地排序,会直接修改调用该方法的列表本身,不返回新列表。sorted(iterable)
:非原地排序,会新建一个列表,并返回排序后的结果,原列表或可迭代对象不变。
14.1 sort(reverse=False)
list.sort()
使列表内的元素从小大排列,直接修改列表本身。如果里面指定 reverse=True
则列表降序排列。
list.sort()
方法会直接修改列表本身,使其从小到大排序(默认 reverse=False
),如果 reverse=True
则降序(从大到小)排序。
注意:sort 只存在于列表类型,也就是只能用于列表。代码如下:
numbers = [2, 1, 4, 3, 7, 6, 5, 0, 9, 8]
numbers.sort()
print(numbers)
numbers = [2, 1, 4, 3, 7, 6, 5, 0, 9, 8]
numbers.sort(reverse=True)
print(numbers)
# ---output---
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
这里有个点需要再次强调,.sort()
是直接修改列表本身。而如果是提取数据时,使用 sort()
排序,排序会成功。但你想用排序后的结果,无法使用。因为你提取的列表没有被存储到变量中,提取的列表存在,但无法被指名使用。例如下面的代码:
14.2 切片与排序:小心“一闪而过”的结果
这里这个概念以及情况比较特殊,我会准备两种讲解方法,自行选择看哪种更适合理解。
14.2.1 方法一
切片排序的“陷阱”——别让结果转瞬即逝
在使用 list.sort()
时,有一个常见的“坑”需要特别注意:切片得到的是一个全新的列表对象,而非原列表的某个“视图”或“子列表”。因此,如果你在一段代码中对切片后的列表调用 .sort()
,即使排序本身完成了,却不会对原列表产生任何影响,更不会留下一个可被引用的变量来保存该“新列表”排序后的结果。举个例子:
numbers = [2, 1, 4, 3, 7, 6, 5, 0, 9, 8]
# numbers[::2].sort() 等价 [2, 4, 7, 5, 9].sort()
numbers[::2].sort() # 这其实是在对 [2, 4, 7, 5, 9] 排序
print(id(numbers), id(numbers[::2])) # id 用来监测变量的物理地址,查看两者的内存地址(id)
numbers[::2]
返回的是一个包含索引为 0、2、4、6... 的全新列表(在这里是[2, 4, 7, 5, 9]
),并不是对numbers
本身进行“局部操作”或“映射”。调用
numbers[::2].sort()
只会对这个临时生成的新列表进行原地排序,可是这个新列表并未被赋值到任何变量,也就无法再被拿来复用。从
id(numbers)
与id(numbers[::2])
的输出可以看到,它们是两个不同的对象。这也印证了切片生成新对象的机制。
因此,如果希望对某个切片进行排序并保留结果,正确的做法是先将切片赋给一个新的变量,再对该变量进行排序。例如:
numbers = [2, 1, 4, 3, 7, 6, 5, 0, 9, 8]
sub_list = numbers[::2] # 先把切片赋值给 sub_list
sub_list.sort() # 对 sub_list 原地排序
print(sub_list) # [2, 4, 5, 7, 9]
这样你就可以明确地保留并使用这个已经排序好的列表,而不是让它“转瞬即逝”。如果你的目的是对原列表的特定位置元素重新排布(如“只对偶数索引的元素进行排序”并写回原列表中),那就需要在排序完切片后,再将它赋值回相应的切片位置上。
TODO: 讲解演示列表直接修改本身和返回生成新的列表的区别和注意点。
14.2.2 方法二
上面的注意点很重要,虽然有重复论证之嫌,我还是再来一个例子。尽可能,让你们理解。
想象你有一张原始照片(列表 numbers
),你想把其中一些人(偶数下标的元素)重新站好队形(排序)。当你执行 numbers[::2].sort()
时,其实相当于:
- 先用切片操作
numbers[::2]
拍了一张新照片,这张照片里只有偶数下标那部分人。 - 再对这张“新照片”里的队形进行调整和排序。
可是一旦拍完这张新照片,你并没有把它保存到任何地方(没有赋值给一个变量),就直接把它扔了——原来的大照片 (numbers
) 也没有跟着更新。于是最终,原照片里的人依然没有变动,你也没有留存这张已经排好队形的照片可供后续使用。
如果你的目标是想真的把“偶数下标那部分人”在原照片里排好队形,就得先用一个变量把拍下来的那张“新照片”保存好,并进行排序,最后再“贴回”原照片。比如:
numbers = [2, 1, 4, 3, 7, 6, 5, 0, 9, 8]
sub_list = numbers[::2] # 拍下“偶数下标”这张新照片
sub_list.sort() # 对照片中的人排序
numbers[::2] = sub_list # 将排好的队形贴回原照片
这样原列表的偶数下标部分就被重新排列了。如果你只是想得到那部分排好序的结果,可以直接对 sub_list
进行操作并使用它就行,但要记得先保留它,否则“一闪而过”的结果就再也找不到了。
14.3 Python 列表切片 [::step]
:创建新列表 vs. 指向原列表特定元素
接下来,我们要研究:list[::step]
代表一个新切片列表,但也代表指向原列表的 [::step]
。
numbers[::step]
是 Python 中切片(slicing)操作的一个示例,它既创建了一个新的列表(新切片列表),也可以看作是指向原列表的某些元素的视图。
我们来详细拆解这个操作的含义,(注意:这里讲解虽然讲 [::step]
,但实则也是在讲:[start:stop]
。)下面我我们将使用 [::2]
来演示。
切片
[start:stop:step]
语法回顾在 Python 中,列表切片的基本语法是:
list[start:stop:step]
其中:
start
:起始索引,默认为0
stop
:结束索引,不包含该索引,默认为len(list)
step
:步长,默认为1
,可以为负数表示反向切片
numbers[::2]
的含义假设有一个列表:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
执行
numbers[::2]
,等价于:numbers[0:len(numbers):2]
它的行为如下:
start
省略,默认0
(从索引0
开始)stop
省略,默认len(numbers)
(直到列表结束)step=2
,表示每隔两个元素取一个
得到的结果:
[0, 2, 4, 6, 8]
也就是说,这个操作会返回索引为
0, 2, 4, 6, 8
的元素,形成一个新的列表。为什么说它是 “新切片列表”?
在 Python 中,切片操作会返回一个 新的 列表,而不是原列表的引用。例如:
new_numbers = numbers[::2] print(new_numbers) # [0, 2, 4, 6, 8]
如果修改
new_numbers
,原来的numbers
不会受到影响:new_numbers[0] = 100 print(new_numbers) # [100, 2, 4, 6, 8] print(numbers) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
这说明
numbers[::2]
生成的是一个新的列表对象。为什么又说它 “指向原列表的
[::2]
”虽然
numbers[::2]
生成了一个新的列表,但它的元素其实是从 原列表的[::2]
位置获取的,也就是它的值来源于原列表中的索引0, 2, 4, 6, 8
,所以可以认为它是对原列表特定索引的一种“投影”或“视图”。但是,和 NumPy 数组不同,Python 列表切片并不会共享底层数据,它会创建一个全新的副本。如果你需要修改原列表中的这些元素,需要使用切片赋值:
numbers[::2] = [100, 200, 300, 400, 500] print(numbers) # [100, 1, 200, 3, 300, 5, 400, 7, 500, 9]
这说明
numbers[::2]
也可以用作索引表达式,直接修改原列表的特定位置数据。关键总结
numbers[::2]
创建了一个新的列表,不影响原列表。numbers[::2]
相当于原列表的部分元素的投影,但数据存储是独立的。如果用
numbers[::2] = [...]
赋值,则直接修改原列表对应的元素。
14.4 sorted(list, reverse=False)
sorted(list, reverse=False)
函数可以传入任意可迭代对象,将可迭代对象进行升序排序。然后返回一个新的、排序好的列表,原列表不变。reverse
默认 False,如果设置为 True 则返回降序排序。
将列表进行小到大排序,排序后原列表不变,返回新列表。
sorted()
函数可以传入任意可迭代对象,同样,可以通过 reverse=True
进行降序排序。
sorted()
返回新的排序列表,不会修改原列表:
lst = [9, 8, 10, 7, 6, 5, 4, 3, 2, 1, 0]
new_lst = sorted(lst)
print(new_lst) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
lst = [9, 8, 10, 7, 6, 5, 4, 3, 2, 1, 0]
new_lst = sorted(lst, reverse=True)
print(new_lst) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
还能排序哪些数据?经常有学生问我:如果是字符的话,如何排序?
14.5 可以排序哪些数据?
14.5.1 .sort()
可以排序的元素
数字(int, float)
直接按从小到大(默认)或从大到小(
reverse=True
)排序。numbers = [5, 2, 9, 1, 5, 6] numbers.sort() print(numbers) # 输出: [1, 2, 5, 5, 6, 9]
按绝对值排序排序:
numbers = [-10, 5, -30, 20] numbers.sort(key=abs) print(numbers) # 输出: [5, -10, 20, -30]
字符串(str)
按 字母顺序(ASCII 顺序) 排序,其实也就是按照字母表顺序,默认区分大小写(大写字母排在小写字母前)。
words = ["banana", "apple", "cherry", "C-char", "A-Char"] words.sort() print(words) # 输出: ['A-Char', 'C-char', 'apple', 'banana', 'cherry']
忽略大小写排序:
words = ["banana", "apple", "cherry", "C-char", "A-Char"] words.sort(key=str.lower) print(words) # 输出: ['A-Char', 'apple', 'banana', 'C-char', 'cherry']
按字符串长度排序:
words = ["apple", "banana", "cherry", "kiwi"] words.sort(key=len) print(words) # 输出: ['kiwi', 'apple', 'banana', 'cherry']
布尔值(bool)
False
视为0
,True
视为1
,因此False
会排在True
前。values = [True, False, True, False] values.sort() print(values) # 输出: [False, False, True, True]
元组(tuple)
列表中的元组 按 第一个元素 排序。
tuples = [(3, "apple"), (1, "banana"), (2, "cherry")] tuples.sort() print(tuples) # 输出: [(1, 'banana'), (2, 'cherry'), (3, 'apple')]
(选学)按第二个元素排序:
tuples.sort(key=lambda x: x[1]) print(tuples) # 输出: [(3, 'apple'), (1, 'banana'), (2, 'cherry')]
列表(嵌套列表)
按第一列排序,代码如下:
lists = [[3, 22], [12, 8], [11, 26]] lists.sort() print(lists) # 输出: [[3, 22], [11, 26], [12, 8]]
按第二列排序,代码如下:
lists.sort(key=lambda x: x[1]) print(lists) # 输出: [[3, 2], [2, 3], [1, 4]]
混合类型(不支持)
不能直接排序不同类型的数据,例如
int
和str
混合,因为没办法比较,不同类型直接除非自定义排序标准,不然不同类型直接以什么标准来比较呢?所以程序会报错,代码如下:mixed = [3, "apple", 5, "banana"] mixed.sort() # TypeError: '<' not supported between instances of 'str' and 'int'
小结:
上面讲解的这些情景,都是一些常见的,还有一些不常见的以后你用到时,自然会去研究。
数据类型 | 是否可排序 | 备注 |
---|---|---|
int 、float | ✅ 可以 | 按数值大小 |
str | ✅ 可以 | 按 ASCII 排序 |
bool | ✅ 可以 | False < True |
tuple | ✅ 可以 | 按第一个元素 |
list | ✅ 可以 | 按第一个元素,支持 key |
混合类型 | ❌ 不可以 | TypeError |
关于上面提到的 ASCII,我会在本书的末尾附上一个常见的 ASCII 表,有兴趣的自行去阅读。
14.5.2 sorted()
可以排序的元素
其实和列表的 .sort()
大体类似,这里我还是提供一下便于读者阅读学习。我也经常阅读,所以在阅读到某些被作者省略的部分时。如果前面的内容理解的还不错时,情况或感觉就还好,如果理解的程度刚刚好或者不深入的情况(有可能还没看太懂的情况下),还是想要靠接下来具体的代码示例来学习,这样会让阅读过程、自学过程更加舒适。
排序字符串
默认按 ASCII 排序,其实可以理解成字母表顺序排序,然后大写字母排在小写字母前。
words = ["banana", "apple", "cherry", "C-char", "A-Char"] sorted_words = sorted(words) print(sorted_words) # 输出: ['A-Char', 'C-char', 'apple', 'banana', 'cherry']
忽略大小写排序:
words = ["banana", "apple", "cherry", "C-char", "A-Char"] sorted_words = sorted(words, key=str.lower) print(sorted_words) # 输出: ['A-Char', 'apple', 'banana', 'C-char', 'cherry']
按字符串长度排序:
words = ["apple", "banana", "cherry"] sorted_words = sorted(words, key=len) print(sorted_words) # 输出: ['apple', 'banana', 'cherry']
注意:如果字符串的长度相同,则会按照字符串开头字母的字母表顺序来排序。注意:如果字符串的长度相同时,因为
sorte()
是稳定排序(Python 的 Timsort 排序算法)。这意味着sorted()
在对长度相同的字符串排序时 并没有按照字典序 排序,而是保留了原始列表中的顺序。写到这里,我突然想到之前给一个学员上课时排序的对象时列表嵌套元组,元组第一个元素长度相同时,则自动以元组第二个元素长度进行排序,如果元组第二个元素的长度也相同,则以字母表顺序排序(这里主要是 ASCII 表顺序,只不过为了方便理解,我直接说字母表顺序)。上面说的这种情况,就需要手动实现了,代码示例如下:
data = [("aa", "banana"), ("bbb", "apple"), ("aa", "cherry"), ("bb", "pear")] sorted_data = sorted(data, key=lambda x: (len(x[0]), len(x[1]), x[1])) print(sorted_data) # 输出:[('bb', 'pear'), ('aa', 'banana'), ('aa', 'cherry'), ('bbb', 'apple')]
排序布尔值
False
视为0
,True
视为1
,所以False
在前。values = [True, False, True, False] sorted_values = sorted(values) print(sorted_values) # 输出: [False, False, True, True]
排序元组(tuple)
按第一个元素排序:
tuples = [(3, "apple"), (1, "banana"), (2, "cherry")] sorted_tuples = sorted(tuples) print(sorted_tuples) # 输出: [(1, 'banana'), (2, 'cherry'), (3, 'apple')]
按第二个元素排序:
sorted_tuples = sorted(tuples, key=lambda x: x[1]) print(sorted_tuples) # 输出: [(3, 'apple'), (1, 'banana'), (2, 'cherry')]
排序嵌套列表
按第一列排序:
lists = [[3, 22], [12, 8], [11, 26]] sorted_lists = sorted(lists) print(sorted_lists) # 输出: [[3, 22], [11, 26], [12, 8]]
按第二列排序:
sorted_lists = sorted(lists, key=lambda x: x[1]) print(sorted_lists) # 输出: [[3, 2], [2, 3], [1, 4]]
❌ 混合类型(不支持)
不能直接排序不同类型的数据(如
int
和str
混合),不同的类型直接你按照什么标准来比较呢?——所以不行。mixed = [3, "apple", 5, "banana"] sorted(mixed) # TypeError: '<' not supported between instances of 'str' and 'int'
sort()
vs. sorted()
sort() | sorted() | |
---|---|---|
适用对象 | 只能用于列表 | 适用于所有可迭代对象(列表、元组、字符串等) |
是否原地排序 | ✅ 会修改原列表 | ❌ 不会修改原数据,返回新列表 |
返回值 | None | 新的排序列表 |
是否可用于元组 | ❌ 不支持 | ✅ 可以 |
是否可用于字符串 | ❌ 不支持 | ✅ 可以 |
是否可用于集合 | ❌ 不支持 | ✅ 可以 |
你应该已经发现了,其实和 .sort()
的示例类似,有略微的差异。但是我还是给你了,原因前面也告诉你了,这里就不过多赘述了。
14.6 切片排序练习
在本练习中,我们将探索如何使用 Python 处理字符串,并利用切片和排序操作来修改列表中的特定元素。
14.6.1 任务描述
- 将一串字符串
'132569874'
转换成列表并将其输出; 对其中偶数下标的元素进行降序排列,奇数下标的元素不变。- 对于列表中的偶数下标(索引为 0、2、4、6、...)的元素进行降序排列,而奇数下标的元素保持不变。
- 输出最终处理后的列表。
14.6.2 学习法则
这个题目在阅读时,请认真思考。不要直接阅读下面的代码实现,先去思考自己独立完成,实在解决不了再继续往下阅读。
这里我分享一个学习法则,你要去悟。真的有时候学习也是需要一定悟性的。
有时候一个问题或者题目写不写的出来不重要,重要的是你进行了有效思考。这样你在继续阅读本书后续讲解的时候,你的思维就会跟着我的讲解走,因为你的大脑是活跃的。你必须要把你的思考和接着阅读后续答案(解答)这个过程是连续的。
就是你思考了,不论你是否做出来,你下一刻就是要继续阅读本书的解答部分,这称为连续的。并不是你现在思考了一下题目,然后去吃饭了。吃完饭再来直接阅读我的解答,这不是连续的。你的有效学习就断开了,这时你需要做的是:重新思考一下问题或题目,再做一遍,让自己的大脑处于那个状态(可以理解为学习、思考、研究状态),然后再接着阅读本书对于这个题目的讲解。反之,直接不思考直接接着阅读,有可能能直接理解,但是思维无法及时的跟随着我的思路走。此时,你看似进行了阅读学习,但实则在未来要独立应用是你无法真正从 0 到 1 开始编写代码。(排除:天才)
14.6.3 何为有效思考?
这里解释一下什么是有效思考,是你经过行动后,经过一定次数的尝试之后,可以得到一定结果的过程。 不论这个结果是否正确,接下来我稍微详细的描述一下:
主动性:不急于看现成的代码或答案,不要让自己的学习成为:被动接受信息,而是先让自己尝试去推理、去构思。我们需要做出足够的思考尝试,并且记录下自己的思路或疑问。即便一时想不出完整解法,也要保持思考的主动性。一定要做到先独立思考!在当下这个人工智能爆发的时代,获取答案很简单。我们真正在学习的过程中,要求索的是得到答案前的过程。(也就是问题和答案直接,存在的过程!)
深入性:不只是表面理解问题,而是深入挖掘问题背后的逻辑、原理、模式等。例如,思考为什么这样做、是否有其他更优解、不同方法之间的优劣对比。
思考并不只在于能否立刻把题目做出来,更重要的是“让脑子动起来”,真正体会到哪些地方容易卡壳、哪里缺少思路、为什么会想不通,这些感受才能在后续学习中派上用场。过程连续:在思考之后,无论是否得出解答,都紧接着去阅读或验证正确答案,其实也是验证自己独立思考过程中,所产生的疑惑或者想法,并将思考的过程与后续阅读结合起来。在这个过程中,大脑会对比自己和作者的思路,产生新的领悟或纠正。
连续可以确保可以让大脑保持“正在研究问题”的状态,如果中途去做其他事情(如去吃饭),那么下次回来就需要重新激发这个思考状态,以保证思维保持“热身”状态并能与后续的讲解衔接。总之你要知道:不是零散的、断断续续的思考,而是一个完整、连贯的过程。思考→尝试解决→对比答案→反思改进→继续学习。
大脑保持活跃:有效思考的目的不在于“是否马上写得出完整代码”,而是“有没有真正激活了自己的思维”。当你带着独立思考的疑问或猜想去阅读答案或讲解时,你的关注点会与作者的观点更好地对接,也更容易吸收和内化知识。
反思与融会贯通:有效思考往往伴随着自我反思:我为什么想不出来?我在哪些关键点没有突破?通过对比作者或书本的讲解,理解自己与正确思路之间的差距,这个过程本身就是思维能力的提升。但这里我要强调一下:或许你的思维和我的思维不一样,这没事。每个人都是独立的个体,每个人的思维都是不同的。虽然都是不同的哈姆雷特,只要你最终的结果是对的,那么你也可以认为你的思维是对的。这个对,是仅限于对自己。
动态反馈:结合已有知识,不断调整和修正自己的思维方式和解题思路。不是仅仅关注对错,而是要关注思考过程中学到了什么、新增了哪些理解。及时的总结,以及做好笔记。
专注度:进入思考的“状态”,避免中断后直接跳读答案,而是重新激活大脑回到思考轨道上。这样可以保证思维的连贯性,提高学习效果。
简而言之,“有效思考”不仅仅是“做过练习”或者“尝试写代码”,更重要的是让思维真正参与到解决问题的过程中,并且与后续阅读、对比、修正、学习的步骤紧密衔接。这种思考让你大脑处于一种“热身”或“活跃”状态,能够更高效地理解和吸收后续内容,从而在学习中获得更深的领悟与成长。
当你始终保持这样连续的“有效思考”,你会发现自己对问题的理解更深入,对解答也吸收得更快;而如果中断过久,直接跳到答案就会变成被动接受,不仅理解得浅,还会大大降低学习效果。
给你总结一个大致的公式:
- 有效思考 ≠ 简单看题目,等着答案
- 有效思考 = 主动探索 + 深入思考 + 连续性 + 反馈调整 + 高度专注
14.6.4 切片排序练习解答(答案)
接下来,回到我们的题目。下面是实现该任务的 Python 代码:
# 将字符串转换为列表
str_to_list = list('132569874')
print("原始列表:", str_to_list)
# 提取偶数下标的元素
even_position = str_to_list[::2]
# 对偶数下标的元素进行降序排序
even_position.sort(reverse=True)
# 重新赋值给原列表对应位置
str_to_list[::2] = even_position
# 输出最终结果
print("处理后的列表:", str_to_list)
代码解析:
- 字符串转换列表:使用
list()
方法将字符串'132569874'
拆分成单个字符组成的列表。 - 提取偶数下标元素:利用
list[::2]
切片获取索引为 0、2、4、6、8 的元素。 - 排序:调用
sort(reverse=True)
对这些元素进行降序排列。 - 重新赋值:将排序后的元素重新放回原列表对应位置。
- 输出结果:分别打印转换后的原始列表和处理后的最终列表。
运行示例:
原始列表: ['1', '3', '2', '5', '6', '9', '8', '7', '4']
处理后的列表: ['8', '3', '6', '5', '4', '9', '2', '7', '1']
14.6.4 思考题
上面代码直接实现了,你阅读下面小悦写的代码,描述下面代码为什么不能正确实现?
# 将字符串转换为列表
str_to_list = list('132569874')
print("原始列表:", str_to_list)
# 提取偶数下标的元素并进行降序排序
str_to_list[::2].sort(reverse=True)
# 输出最终结果
print("处理后的列表:", str_to_list)
# ---output---
原始列表: ['1', '3', '2', '5', '6', '9', '8', '7', '4']
处理后的列表: ['1', '3', '2', '5', '6', '9', '8', '7', '4']
这题我不给解答,具体的原因前面也讲解过。先思考,再去查阅本节的 14.2 切片与排序:小心“一闪而过”的结果
14.6.5 练习拓展
为了进一步加深理解,你可以尝试以下变体练习:
Question 1:修改代码,使奇数下标的元素进行升序排列,而偶数下标的元素保持不变。
Question 2:让用户输入任意长度的数字字符串,并按相同规则处理。
通过这些练习,你可以更加熟练地掌握 Python 中的列表切片、排序和元素赋值技巧,为后续的编程实践打下坚实的基础!
这里我还是会提供对应代码,便于你参考。
Question 1 Solution:
# 将字符串转换为列表
str_to_list = list('132569874')
print("原始列表:", str_to_list)
# 提取奇数下标的元素
odd_position = str_to_list[1::2]
# 对奇数下标的元素进行升序排序
odd_position.sort()
# 重新赋值给原列表对应位置
str_to_list[1::2] = odd_position
# 输出最终结果
print("处理后的列表:", str_to_list)
Question 2 Solution:
# 用户输入字符串
user_input = input("请输入一串数字: ")
str_to_list = list(user_input)
print("原始列表:", str_to_list)
# 提取偶数下标的元素
even_position = str_to_list[::2]
# 对偶数下标的元素进行降序排序
even_position.sort(reverse=True)
# 重新赋值给原列表对应位置
str_to_list[::2] = even_position
# 输出最终结果
print("处理后的列表:", str_to_list)
15. reverse():颠倒乾坤,逆转列表
反转列表中的元素。
在 Python 中,reverse()
方法可以直接在原列表上进行原地反转,使其元素顺序颠倒。它不会创建新的列表,而是直接修改原列表的顺序。
语法:
列表.reverse()
参数说明:
reverse()
方法不接受任何参数。它的作用是将列表中的元素就地反转,而不会返回新的列表对象。
# 定义一个药品列表
lst = ['毒药', '解药', '感冒药']
# 使用 reverse() 方法进行反转
lst.reverse()
# 输出结果
print(lst) # ['感冒药', '解药', '毒药']
在这个示例中,列表 lst
的元素顺序被彻底翻转,最开始的第一个元素 '毒药' 变成了最后一个,而最后一个元素 '感冒药' 则变成了第一个。
注意事项:
原地修改,不返回新列表
reverse()
直接修改原列表,不会返回新列表,因此不能写成new_lst = lst.reverse()
,因为reverse()
方法返回的是None
。如果想要得到一个反转后的新列表,可以使用[::-1]
切片操作:lst = ['毒药', '解药', '感冒药'] new_lst = lst[::-1] # 使用切片创建反转后的新列表 print(new_lst) # ['感冒药', '解药', '毒药']
与
reversed()
的区别reverse()
是列表的方法,直接修改原列表,而reversed()
是一个内置函数,返回的是一个反向迭代器,而不会修改原列表:lst = ['毒药', '解药', '感冒药'] rev_lst = list(reversed(lst)) # 需要用 list() 转换为列表 print(rev_lst) # ['感冒药', '解药', '毒药'] print(lst) # ['毒药', '解药', '感冒药'] (原列表不变)
应用场景:
翻转数据列表:在处理数据时,可以使用
reverse()
方法对数据列表进行颠倒,如反转时间序列数据。游戏开发:在某些游戏逻辑中,可能需要翻转某个列表,例如倒序显示排行榜信息。
算法实现:在某些算法中,如回溯法或者栈的模拟,可以利用
reverse()
来调整数据顺序。
小结
reverse()
直接修改原列表,反转元素顺序。- 它不返回新列表,而是返回
None
。 - 如果需要创建一个新的反转列表,可以使用
[::-1]
切片或reversed()
函数。
16. 列表的深浅拷贝
16.1 y = x
所存在的问题,是真的备份吗?
阅读下面的代码,思考下面两个问题:
问题1:想一下下面的程序在做什么?也就是开发者这么写代码的意图是什么?- 问题1:这段代码的意图是什么?开发者希望实现什么效果?
- 问题2:代码的输出结果是什么?是否符合预期?
注意: 下面的代码我是有意写这么长的字符串,有两个目的:
- 目的一:为了带你回顾一个字符串格式化、转义的用法!
- 目的二:方便我们后续观察找出问题;
x = ['毒药', '感冒药', '解药']
y = x
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y: {id(y)}') # id() 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y: {id(y)}')
阅读到此,想必你有了你自己的思考。我们现在来看看,我对于上面两个问题的回答。
16.1.1 问题1
从代码 y = x
可知,开发者想要复制一个 x 列表的副本给变量 y 并且想要修改列表 y 时不影响原列表 x。换句话说,y 应该是 x 的一个独立备份。
为什么我们会有这种需求?让我们结合现实场景来思考。
假设你有一个包含 100万条数据的 Excel 文件,为下载这个数据文件获取这个文件的代价极高:
- 你需要花费1000元;
- 下载耗费3个小时;
- 并且1000元只能下载一次,数据丢失想要重新下载需要重新付费;
注意:这里我是特意这么夸张,请不要想其它的。我只是为了让你尽可能感觉到这个数据是多么昂贵的,并且获取这个数据所要付出的代价是巨大的,因为只有昂贵才会重视,便于我塑造情景。
这里再次强调:如果再下载一次还是需要 1000元、下载耗费3个小时、并且还是只能下载一次。此时你要操作这个拥有 100万数据且下载成本极高的 Excel 会怎么操作?
阅读到此时,请停顿一下。思考一下,在这种情况下,你会怎么做?
毫无疑问,你会在开始操作前 先备份一份数据,以便在出现意外(例如误删除、文件损坏、系统崩溃等)时,可以恢复到原始状态,而不必重新花费巨大的代价去重新获取数据。
毫无疑问,肯定是在操作前做一个备份,以便在操作时:如果不小心手贱按错按键,或者破电脑突然卡住导致你操作的数据没有及时保存或关闭等情况,导致数据缺失或损坏。此时就需要使用备份文件来恢复,这样就避免重新获取数据而产生的高额代价。所以在上面的代码中也是这样的意图,列表 x 作为备份数据,以确保对 y 的修改不会影响到原始数据 x,以此保证原数据不被影响。
16.1.2 问题2
对于问题2请先观察下面的代码实际输出,看看输出结果中存在什么问题和特点?
注意:观察下面的代码时你要具备一个原则:有时候,不要只看局部。站的高点,站的远点,使我们有全局视角,这样往往更便于我们发现事物中所存在的关联关系或规律。
意思就是:你在观察下面的输出结果时,不要单纯的一行一行看,或者看完下一行忘记上一行。要把自己从中抽离出来,看整个输出。去及时的观察和对比,这样才能发现其中的规律或者存在的问题。
Original:
x: ['毒药', '感冒药', '解药']
y: ['毒药', '感冒药', '解药']
id_x: 4470743680 id_y:4470743680
After:
x: ['消炎药', '感冒药', '解药']
y: ['消炎药', '感冒药', '解药']
id_x: 4470743680 id_y:4470743680
我们会发现,~~修改 y 的列表,但是会同时影响 x 列表。~~当我们修改 y 列表中的第一个元素后,x 列表的内容也发生了变化,这显然不符合我们的预期。理论上,我们的目标是 仅修改 y,不影响 x,但结果表明 x 和 y 竟然是同一个列表!
为什么会产生这个问题情况呢?我们可以从 id()
函数的输出中找到答案。
赋值前后,id_x
和 id_y
物理地址的值是 相同的,说明 x
和 y
其实指向的是 同一个列表对象。也就是说,y = x
这个语句 并没有创建一个新的列表,而是让 y
直接引用了 x
原来的数据。
从上面的输出结果可知:列表 x 和列表 y 的物理地址是相同的,意味着虽然做了备份(y = x
),但没想到列表 x 和 y 是指向同一个列表,故而导致修改列表 y 时,列表 x 的数据也被修改。与我们原本的意图(备份)事与愿违,并且产生了“巨大”代价!明显我们的代码不符合预期。
~~是因为,在进行 y = x
的赋值时,~~从上面的分析后,我们可知 y = x
只是进行了列表地址的赋值(类似引用),x、y 实际上指向的是同一个列表。可以理解成两个列表元素一样,Python 就偷懒不再创建一个新的相同列表,直接创建一个地址引用。
为了辅助理解,我举个生活中的例子:
为了更直观地理解这个问题,我们可以借助一个现实生活中的类比:
- 假设你在百度网盘中 已经上传了一个文件,然后你又想要“上传”相同的文件。
- 但百度网盘发现,这个文件已经存在,它 不会真正再上传一次,而是 直接在你的账户中创建一个链接,指向已经存在的文件。
- 这样,你看到自己网盘中有两个相同的文件,但实际上它们 指向的是同一个数据,修改其中一个,另一个也会随之变化。
百度网盘中如果文件已经存在网盘里面(不分存在哪个账户中),则会直接和你的账户进行链接,从上传速度也可以发现。
Python 在 y = x
赋值时的行为类似于百度网盘的“创建链接”机制:并没有真正复制列表,而只是让 y
指向了 x
,所以 x
和 y
变成了 同一个对象的两个引用。
16.1.3 归整一下
上面分析了一下,接下来我来给你归整归整。我们说列表 x 和列表 y 指向的是同一个变量,我们口说无凭,来看看下面的凭证:
证明一:Python
id()
用来检查变量物理地址(也就是在计算机中,所在的位置)并结合上面的代码结果可知:x、y 指向的是同一个列表,因为 id 相同。(id_x: 4470743680
、id_y:4470743680
)证明二:可以直接使用可视化:https://pythontutor.bornforthis.cn/visualize.html#mode=edit查看,毕竟一图胜千言。在可视化界面中(下图),你会发现
x
和y
其实是 同一个列表对象的两个名称,本质上它们没有区别。因此,修改y
也会影响x
,这就是问题的根本原因。

这里在正式讲解解决方法之前,我先
有时候,不要只看局部。站的高点,站的远点,使我们有全局视角,这样往往更便于我们发现事物中所存在的关联关系或规律。——AI悦创 2024-01-01 23:17:10
16.1.4 关键问题:如何正确创建副本?
既然 y = x
不能满足我们“创建副本”的需求,那么 如何正确地复制一个列表,而不影响原列表呢?
别担心,我们接下来就会深入探讨 Python 中的深拷贝与浅拷贝,以及 如何避免此类问题。
既然上面存在问题,我们应该如何解决呢?
16.2 使用 copy()
进行浅拷贝
16.2.1 代码实现
在前面的内容中,我们发现 直接赋值 y = x
并不会创建一个新的列表,而只是让 y
指向 x
,导致修改 y
的同时 x
也会被修改。这显然不符合我们的备份需求。
那么,如何正确创建一个新的副本,确保 y
和 x
互不影响呢?为了解决这个问题,我们可以使用列表自带的 copy()
方法,这会创建一个“浅拷贝”(shallow copy),从而获得一个独立的对象副本。
答案之一是使用 让我们看看使用 copy()
方法。copy()
进行列表复制的效果:
x = ['毒药', '感冒药', '解药']
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y:{id(y)}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After:\n\tx: {x}\n\ty: {y}\n\tid_x: {id(x)}\tid_y:{id(y)}')
代码输出如下:
Original:
x: ['毒药', '感冒药', '解药']
y: ['毒药', '感冒药', '解药']
id_x: 4428223616 id_y:4428537536
After:
x: ['毒药', '感冒药', '解药']
y: ['消炎药', '感冒药', '解药']
id_x: 4428223616 id_y:4428537536
16.2.2 结果分析
从输出结果来看,我们可以发现两个重要的现象:
id_x
和 id_y
变了:这意味着 copy()
方法 确实创建了一个新的列表,y
和 x
现在是两个独立的对象,互不影响。
id_x
和id_y
变了:初始状态下,x
和y
的内容完全相同,但物理地址(id_x
和id_y
)已经 不再相同,这表示copy()
方法为y
创建了一个全新的列表对象。y
和x
现在是两个独立的对象,互不影响。- 修改
y
后,x
没有被影响:之前y = x
时,修改y[0]
为'消炎药'
时会影响x[0]
,但现在y
已经是x
的一个独立副本,因此修改y
不会影响x
,这正是我们想要的备份效果。
当修改 y[0]
为 '消炎药'
时,x
保持不变,只有 y
被修改。这正是我们想要的 彼此独立 的效果。
16.2.3 为什么说是“浅拷贝”?
copy()
方法在 Python 中执行的是 浅拷贝(Shallow Copy)。它的本质是 创建一个新的列表对象,并将原列表中的元素引用复制到新列表中。简单来说:
copy()
复制了列表本身,即创建了一个新的y
。- 但 它并不会复制列表中的元素,而是让
y
里的元素仍然指向x
里的同样的元素。如果列表中包含可变的嵌套对象(如列表里还有列表、元组、字典等),这些 子对象依旧是原来对象的引用。也就是说,对同一层级的数据做了拷贝,但 更深层次 的数据结构还会被引用到原对象。 - 对于目前这种 单层列表,我们可以把
.copy()
当作和“深拷贝”一样使用,但是前提是:列表的元素都是字符串、数字型、布尔型等不可变数据类型,并没有深层的嵌套结构,自然也就不会出现意料之外的相互影响。
这可能有点绕,我们可以用下面的图示帮助理解:
原列表 x:
x ---> ['毒药', '感冒药', '解药']
↑ ↑ ↑
新列表 y: | | |
y ---> ['毒药', '感冒药', '解药']
可以看到,x
和 y
是两个不同的列表对象(id()
值不同),但它们里面的元素其实是 指向同一个数据的引用。
16.2.4 copy()
的适用场景
✅ 适用于:
复制一个一维列表,确保y
是x
的副本,修改y
不会影响x
。- 对于普通的、一维的或者元素不可变的列表,使用
x.copy()
(或x[:]
)就足够解决“大部分”复制需求。
❌ 不适用于:
- 复制嵌套列表(二维或多维列表)。因为
copy()
仅复制了外层列表,如果列表内部还包含可变对象(如子列表),这些子列表仍然是共享的,即修改y
内部的子列表仍然会影响x
。
16.3 copy()
的局限性:浅拷贝的陷阱
上面我们解决了备份列表 x,成功的防止在修改 y 列表时影响到列表 x。但是我们又遇到新的问题了,按照惯例继续先阅读下面的代码,然后思考下面的代码在做什么?结果有什么出乎意料的?
在前面的讲解中,我们为列表 x
创建了一个副本,从而避免了修改 y
时影响 x
的情况。然而,这里又引出了一个新的问题:为什么在有些情况下,修改 y
仍旧会影响到 x
?请先阅读下面的代码,并思考它在做什么,最终产生了怎样的结果,这个结果是否出乎你的意料。
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
运行结果如下所示:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
After 2:
x: ['毒药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
id:
id_x: 5306734464 id_y: 5310717568
id_children x[3]: 4347684800
id_children y[3]: 4347684800
16.3.1 解析:为什么 x
也被修改了?
在 After 2
这一步,我们仅修改了 y
中子列表的内容后(y[3][0]
),理论上 x
不应该受到影响。但实际结果显示,x
的对应部分也跟着改变了,也就是 x[3][0]
也被修改成了 '苹果'
,这是因为 .copy()
方法只执行了浅拷贝(shallow copy),它并不会深入到子列表或更深层级的数据结构去创建新的副本,而是让 x
和 y
共享同一个子列表。
浅拷贝的工作机制:
.copy()
仅复制了x
的 第一层结构,但不会递归地复制内部的嵌套列表。也就是说,
y[3]
仍然指向x[3]
的 同一个对象,它们共享相同的内存地址。这可以通过id(x[3]) == id(y[3])
来验证,两者的内存地址完全一致。
可以从以下两个方面来验证这一点:
子列表 ID 相同:输出中可见
x[3]
和y[3]
的id
完全一致,这说明它们实际上引用的是同一个对象(子列表)。你还可以为上面的代码添加一行代码来更直观的验证:print(id(x[3]) == id(y[3]))
。可视化关系相同:若从可视化角度来观察,可以看到
x
和y
在最外层各自拥有不同的地址,但它们第三个索引位置(子列表)都指向同一个存储空间。还是使用:https://pythontutor.bornforthis.cn/visualize.html#mode=edit,图示如下:证明一:从上面的代码可知,子列表的 id 是相同的,代表 x 和 y 的子列表是同一个列表证明二:从可视化可知

如果想要彻底独立的副本(包括子列表也要复制),就需要使用 深拷贝(deep copy)
所以,copy 实现的是浅拷贝,只拷贝列表的第一层,嵌套的列表则不会拷贝。
因此,如果你希望获得完全独立的副本(包括子列表和其他更深层的嵌套对象都相互独立),则需要使用深拷贝(deep copy)。
16.4 deepcopy()
:如何彻底复制列表?
使用深拷贝需要导入库:
为了使用深拷贝功能,首先需要导入 copy
模块中的 deepcopy
函数:
from copy import deepcopy
下面用一个示例演示深拷贝的效果:
from copy import deepcopy
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = deepcopy(x)
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\n\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
示例输出如下:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
After 2:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['苹果', '瓜子', '八宝粥']]
id:
id_x: 4408501568
id_y: 4408501056
id_children x[3]: 4408175296
id_children y[3]: 4408501312
16.4.1 解析:为什么 deepcopy()
解决了问题?
deepcopy()
会递归地复制整个数据结构,而不仅仅是第一层。x[3]
和y[3]
现在是两个不同的列表,修改y[3]
不会影响x[3]
。
如何证明上面的结论呢?依然使用的是两种方法:
子列表 id 不一样,所以不会被修改了;
- 方法一:子列表的
id
值已经不同,说明x
和y
中的子列表各自拥有独立的内存空间。你还可以基于上面的代码,通过添加print(id(x[3]) != id(y[3]))
得到 False 可以验证,它们的 ID 已经不同。id 即代表物理地址,物理地址不同则不是同一个数据。 - 可视化层面也可以看出,被拷贝的多层嵌套结构完全分离,互不影响,一图胜千言:

使用的网站依然是:https://pythontutor.bornforthis.cn/visualize.html#mode=edit
16.5 一个特殊情况:浅拷贝时不会影响原数据的情况
虽然前面提到浅拷贝不会递归复制子列表(子列表没有完全 copy 出来),导致修改 y
时也会影响到 x
,但在某些特定情况下,浅拷贝的改动并不会影响原列表,也就是修改列表 y 是不会影响列表 x。
请先思考一下:
我们上面说了列表的深浅拷贝,但是浅拷贝的时候,虽然子列表没有完全 copy 出来,但是有一种情况下修改列表 y 是不会影响列表 x。是不会互相影响。
所以,接下来依然是思考一下🤔: 也就是说,在浅拷贝的代码中,什么情况下修改子列表不会互相影响。 在浅拷贝的代码示例中,在哪种情形下我们修改了 “子列表” 却不影响另外一个列表?
下面我也提供了浅拷贝代码,请认真激发你的有效思考:
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
y[3][0] = '苹果'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
既然,我们修改引用的子列表会互相影响。那我们大胆一点想一想,直接把整个列表修改了,是不是就不会互相影响了呢?下面这段代码提供了线索。请对比与前面代码的差异之处,并观察最终输出:
x = ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y = x.copy()
print(f'Original:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}') # id 用来获取变量的物理地址
y[0] = '消炎药'
print(f'After 1:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
# y[3][0] = '苹果'
y[3] = 'www.bornforthis.cn'
print(f'After 2:\n\tx: {x}\n\ty: {y}\n\tid:\n\t\tid_x: {id(x)}\t\tid_y: {id(y)}\n\t\tid_children x[3]: {id(x[3])}\n\t\tid_children y[3]: {id(y[3])}')
运行结果如下:
Original:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4349618112
After 1:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4349618112
After 2:
x: ['毒药', '感冒药', '解药', ['香蕉', '瓜子', '八宝粥']]
y: ['消炎药', '感冒药', '解药', 'www.bornforthis.cn']
id:
id_x: 4412075712 id_y: 5778383360
id_children x[3]: 4349618112
id_children y[3]: 4698471152
从上面的输出可以得知,当直接把 y[3]
替换成一个新的字符串('www.bornforthis.cn'
)时,x
中并不会受到影响,因为此时我们并没有去修改原本共享的子列表,而是让 y
指向了一个全新的对象,也意味着 y[3]
指向了一个新的内存地址。
对于 “浅拷贝” 而言,列表 x 的 x[3]
仍然指向原来的列表 ['香蕉', '瓜子', '八宝粥']
,并不会受到影响。只要针对某个索引重新赋值为新的对象,原列表对应位置的引用不会跟着变动,自然也就不会再相互影响。
简而言之,浅拷贝只会在“修改同一个嵌套对象本身”时产生联动影响;如果是直接给某个嵌套对象赋予一个新引用,则不会影响到原列表。
通过上面两个示例的对比,我们可以清晰地看到浅拷贝与深拷贝的区别,也了解到在一些特殊操作方式下,浅拷贝的修改并不会波及到原列表。一般而言,如果你只关心最外层结构的复制,且内部嵌套对象可以共享,那么浅拷贝就够用了;而当你需要创建完全独立的副本时,深拷贝才是更合适的选择。
16.5.1 结论
结论一:
copy()
只能进行浅拷贝,嵌套对象不会被复制。结论二:
deepcopy()
进行递归复制,完全独立于原对象。结论三:浅拷贝仅在修改嵌套对象时才会导致意外影响,直接替换元素不会影响原列表。
17. 列表常见的内置函数表
前面我已经把列表常用的一些内置函数讲解了,如果要全部讲解是不可能的。每个领域每门学科,在我们初步入门时,想要一次性学完一次性掌握是很难的(不可能)。那什么是正确的方法呢?掌握所有要学最核心最基本的,剩余其它多余的在未来需要的时候,再去查阅学习,才是正确的。也是我经常说的:学习要有量,也要有克制。
下面是 Python 列表常见的内置函数表格,我稍微汇总了一下便于未来你需要时可以快速查询。想要真正掌握,还是需要尽可能在学习本书时,训练好原则。也就是学习的方法,我会在本书每个部分尽可能引入引导,你需要的则是配合本书的计划进行思考,才能把此 Python 学的更好。
序号 | 函数名 | 函数用法说明 | 代码演示 |
---|---|---|---|
1 | append() | 向列表末尾添加一个元素 | lst.append(4) |
2 | extend() | 将一个列表中的所有元素添加到另一个列表中 | lst.extend([4, 5]) |
3 | insert() | 在指定位置插入一个元素 | lst.insert(1, 'a') |
4 | remove() | 删除列表中的第一个匹配项(指定的值) | lst.remove('a') |
5 | pop() | 删除并返回列表中的一个元素(默认最后一个元素) | lst.pop() |
6 | clear() | 清空列表 | lst.clear() |
7 | index() | 返回列表中指定元素的第一个匹配项的索引 | lst.index('a') |
8 | count() | 计算列表中某个元素出现的次数 | lst.count(1) |
9 | sort() | 对列表中的元素进行排序(默认升序) | lst.sort() |
10 | reverse() | 反转列表中的元素顺序 | lst.reverse() |
11 | copy() | 返回列表的浅复制 | new_lst = lst.copy() |
12 | len() | 返回列表中的元素数量 | len(lst) |
13 | sum() | 计算列表中所有数字元素的总和(元素需要是数字类型) | sum([1, 2, 3]) |
14 | min() | 返回列表中的最小值(元素需要可比较) | min([1, 2, 3]) |
15 | max() | 返回列表中的最大值(元素需要可比较) | max([1, 2, 3]) |
16 | sorted() | 返回列表排序后的新列表 | sorted([3, 1, 2]) |
17 | all() | 检查列表中所有元素是否都为真(非零或非空) | all([1, 2, 3]) |
18 | any() | 检查列表中是否存在至少一个真值(非零或非空) | any([0, 0, 1]) |
19 | enumerate() | 枚举列表,返回索引和元素的元组 | list(enumerate(['a', 'b', 'c'])) |
20 | zip() | 将多个列表中相同位置的元素配对,形成新的元组列表 | list(zip([1, 2], ['a', 'b'])) |
21 | map() | 对列表中的每个元素应用一个给定的函数,返回结果的迭代器 | list(map(str, [1, 2, 3])) |
22 | filter() | 过滤列表中的元素,只返回使给定函数为真的元素 | list(filter(lambda x: x > 1, [1, 2, 3])) |
这些函数涵盖了列表处理的大部分需求,从基本的添加和删除操作到更复杂的排序和转换处理。
17.1 map() 函数
这里着重讲一下 map()
函数,便于本书后续内容的讲解。
假设我们有如下列表:
string_num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
现在需要对上面列表元素求和,但是你会发现:列表里面的元素都是字符串并不能直接求和,需要把列表中的所有元素转换成数字型后才能直接使用 sum()
函数求和。
按我们现在所学的知识,很大一部分学生会想到如下方法:
num1 = int(string_num[0])
num2 = int(string_num[1])
num3 = int(string_num[2])
num4 = int(string_num[3])
num5 = int(string_num[4])
num6 = int(string_num[5])
num7 = int(string_num[6])
num8 = int(string_num[7])
num9 = int(string_num[8])
num10 = int(string_num[9])
上面转换方法,虽然实现。但是要求和只能手动一个一个变量相加:
print(num1 + num2 + num3 + num4 + num5 + num6 + num7 + num8 + num9 + num10) # 输出: 45
上面做了什么行为?——把列表里面的每个元素提取出来,并通过 int()
函数的“洗礼”(强制转换)。就类似现实生活中:我们买回一袋土豆,每个土豆都还带着泥土,不能直接下锅。这时候你需要一个个清洗干净,才能用于做菜。上面的做法就是你自己一个个手洗每个土豆,虽然能洗,但太麻烦了。
那么,有没有更简洁的方法呢?使用 map()
函数,map()
函数就像一个自动清洗机——你只需要把整袋土豆倒进去(也就是你的列表),告诉机器要怎么清洗(也就是指定一个函数),机器就会自动帮你把每一个土豆都洗干净,再整齐地送出来。是不是省时省力又整洁?
上面土豆的例子还是偏抽象,再举个例子:有一辆小三轮车从外面收客户要的快递(当做列表),回到快递站点后准备登记发出(寄出去),里面的每个快递都必须要经过安检器检查(函数)。此时,快递小哥只需要把三轮车对接滑动的皮带,会自动把三轮车上的所有快递,一个一个运输到安检器检查。这个过程就是 map()
函数实现的功能。
接下来,我们来详细讲讲 map()
函数可以用来批量对可迭代对象(列表、元组、字符串等)中的每一个元素执行相同的操作。它的基本语法如下:
map(函数, 可迭代对象)
这句话可以理解为:让“函数”这个工厂,批量处理“可迭代对象”中的每一个元素,并返回一个“加工后”的迭代器。说的通俗易懂一些:把列表中每个元素,拎出来一个就拿 map 中指定的函数转换,拎出来一个就再拿 map 中指定的函数进行转换。
举个例子,在本案例中,我们想把字符串列表里的每个元素都转换为整型,就可以写成:
string_num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
num_list = list(map(int, string_num)) # 将所有元素批量转换为 int
print(num_list) # 结果: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(sum(num_list)) # 结果: 45
在 Python 3 中,map()
返回的并不是列表,而是一个“可迭代对象”(iterator),如果需要在之后的代码中多次使用结果或者想一次性查看所有加工后的元素,一般会用 list()
再把它转换成列表。
可以看到,使用 map()
一行代码就能完成所有元素类型转换的操作,而且再配合 sum()
,我们只需要一句 sum(map(int, string_num))
便能快速得到求和结果。与之前一个个变量强制转换、再手动加起来的方式相比,简洁、明了,而且也减少了出错的机会。
后续在处理类似“批量转换”或“批量处理”需求时,map()
都是一个非常常用且高效的工具,你只需要指定“加工函数”和“原料列表”即可。后续的章节中,还会经常看到 map()
的身影,例如,和其他内置函数结合,实现更灵活的功能。函数还可以是自己定义函数,后续讲到函数,本书也会给你演示。请一定对它保持关注。
18. 练习题
18.1 查找最大和最小值
题目:给定一个列表 prices = [100, 180, 260, 310, 40, 695, 535]
,找出列表中的最大值和最小值,并打印它们的位置(索引)。
看完题目后,你就可以自己开始编写代码了,不要接着阅读下面的提示。除非你想不出来,我为你提供了下面的提示:
- 提示一:
max(list)
、min(list)
得到列表的最大值和最小值; - 提示二:
index(list)
得到列表元素的下标; - 提示三:使用排序方法实现,例如
.sort()
或sorted()
;(在这题中.sort()
并不合适,你可以尽情思考一下)
注意:在学习编程的过程当中,每个人编写的代码不一定都一样,在编程中没有标准代码(答案)这个说法。毕竟一千个人就会有一千个哈姆雷特。只要你的代码实现的结果是正确的,过程不一样没事。这就像中文一般,每个人都会中文,但并不是每个人都能把中文写好,写成优美的文章。
不过,虽然每个人的代码不一样,但我们可以学习好的代码的逻辑,来以此对比改进我们自身的逻辑,使之更好。如同我们阅读作家写的书籍,或许阅读后记不住全部内容。但是在未来你再次书写时,你以往阅读的内容都会滋润你的书写与表达,间接的反哺了你。你的文字表达自然而然的润物无声,代码实现的逻辑也会更加优秀。
此题目的解决方法我提供两个,先来统一聊一下解题思维。题目要找出最大值和最小值的位置,那么我们可以如下拆解问题:
- 大问题:找到列表最大值和最小值的位置;
- 问题拆解:
- Step1:要找到最大值和最小值的位置,前面一步或者说先决条件是要先:完成找到列表中的最大值和最小值;(类似要包饺子一样,先决条件是先准备饺子皮和饺子馅)
- Step2:那么找到列表的最大值和最小值有两种方法:一种是直接使用 Python 提供的
max()
和min()
。另一种则是先对列表排序,那么最大值和最小值就会在排序列表后的第 0 位和最后一位 -1(也可以是len(sorted_prices) - 1
)。接着就要提取排序后列表中的首尾的数值,使用.index()
对未排序的 prices 列表进行查询。这也是为什么此题不适合使用.sort()
原因,因为.sort()
会直接排序原本的列表。
方法一:使用获取最大值 max()
和最小值 min()
的函数来获取,并接着使用 .index()
函数来得到在列表中出现的第一次下标。代码如下:
# 假定列表
prices = [100, 180, 260, 310, 40, 695, 535]
# 手动寻找最大和最小值及其索引
max_price = max(prices)
min_price = min(prices)
min_index = prices.index(min_price)
max_index = prices.index(max_price)
print(f"最小值:{min_price}, 位置:{min_index}")
print(f"最大值:{max_price}, 位置:{max_index}")
方法二:既然是要得到最大值和最小值的下标,那么先对列表进行排序,再获取排序后列表首位的最大值和最小值,接着使用 .index()
进行排序。这里使用 sorted()
排序,好处就是:不改变原列表的情况下,生成排序好的新列表。代码如下:
# 假定列表
prices = [100, 180, 260, 310, 40, 695, 535]
sorted_prices = sorted(prices)
min_price = sorted_prices[0]
max_price = sorted_prices[-1]
max_index = prices.index(max_price)
min_index = prices.index(min_price)
print(f"最小值:{min_price}, 位置:{min_index}")
print(f"最大值:{max_price}, 位置:{max_index}")
基于上面的答案,如果非要使用 .sort()
函数来解决呢?——思考一下,你会如何操作。
想必阅读到此行,你已经花费足够的时间思考。我们来说明不能使用 .sort()
排序的内在原因:因为我们需要在找到最大值和最小值后,进行索引定位。题目需要的是获取未排序列表中,最大值和最小值的位置。故而在这个前提下,直接排序原列表 prices 就会失去原列表的数据。
基于上面所说的问题,我们该如何解决呢?我们使用备份的思想,把原本列表备份一下,然后再进行操作。代码是实现如下,你看看实现的对不对呢?
# 假定列表
prices = [100, 180, 260, 310, 40, 695, 535]
backup_copy_prices = prices
backup_copy_prices.sort()
min_price = backup_copy_prices[0]
max_price = backup_copy_prices[-1] # or backup_copy_prices[len(sorted_prices) - 1]
max_index = prices.index(max_price)
min_index = prices.index(min_price)
print(f"最小值:{min_price}, 位置:{min_index}")
print(f"最大值:{max_price}, 位置:{max_index}")
你觉得上面的代码正确吗?有什么问题呢?认真阅读观察一下,最好自己打一遍。
上面的代码输出结果如下:
最小值:40, 位置:0
最大值:695, 位置:6
从最终的结果可知,最大值、最小值的下标是错的。正确结果应该是最大值下标为 5,最小值的下标为 4。
那么为什么会导致上面的问题呢?这其实就是前面所提到的列表直接复制会产生的问题。这里就不再进行赘述分析,如果忘记的话,再去阅读本书的16. 列表的深浅拷贝。下面直接给出修复代码:
# 假定列表
prices = [100, 180, 260, 310, 40, 695, 535]
backup_copy_prices = prices.copy()
backup_copy_prices.sort()
min_price = backup_copy_prices[0]
max_price = backup_copy_prices[-1] # or backup_copy_prices[len(sorted_prices) - 1]
max_index = prices.index(max_price)
min_index = prices.index(min_price)
print(f"最小值:{min_price}, 位置:{min_index}")
print(f"最大值:{max_price}, 位置:{max_index}")
输出如下:
最小值:40, 位置:4
最大值:695, 位置:5
18.2 列表元素去重
题目:给定一个包含重复元素的列表 numbers = [1, 2, 2, 3, 4, 4, 5]
,创建一个新列表,其中包含原列表的每个元素,但去除重复项,最后输出去重后的列表。
此题我给你个小提示:set(list)
将列表转换成集合,集合具有去重的特性;
这个题目也是照例分析:
- 大问题:对列表进行去重(去掉重复的元素)
- 问题拆解:
- Step1:如何去掉重复的?按照已经所学的知识,还没有涉及循环语法,则不考虑。“去重”这是关键,有哪个数据类型符合这个要点的呢?思考一下。——集合符合这个需求,借助集合的特点:互异性(元素不重复,重复会自动去除)
- Step2:使用集合
set()
转换后会得到集合,而我们最终的目的是得到列表,那么我们应该怎么办? - Step3:还是可以使用
list()
函数进行转换回来。
这题比较简单,我直接给出如下代码:
# 假定列表
numbers = [1, 2, 2, 3, 4, 4, 5]
# 使用集合去重,然后转回列表(注意:这会丢失原始顺序)
unique_numbers = list(set(numbers))
print(unique_numbers)
18.3 合并列表并排序
题目:给定两个列表,合并这两个列表并按升序排序。
这个题目更加简单,就不过多的分析,直接自行思考一下,实现即可。
不过这里我给出我能想到的几种常见实现方法,期待更多其它的方法。记住:你自己的思维也很重要。
方法一:直接使用加法 +
运算符实现,代码如下:
# 假定列表
list1 = [1, 10, 4, 2]
list2 = [8, 3, 7, 5]
# 合并列表
merged_list = list1 + list2
# 排序列表
sorted_list = sorted(merged_list)
print(sorted_list)
方法二:直接使用 .extend()
实现:
# 假定列表
list1 = [1, 10, 4, 2]
list2 = [8, 3, 7, 5]
# 合并列表
list1.extend(list2)
# 排序列表
sorted_list = sorted(list1)
print(sorted_list)
ok,祝贺你学完本章节。再接再厉,加油!
更新日志
92f62
-于f6981
-于1be89
-于87164
-于41e05
-于58b72
-于1c35a
-于d2a97
-于d1995
-于69e7f
-于e4b07
-于6b02b
-于bd6f0
-于72488
-于4c208
-于cdfcd
-于0a075
-于342e0
-于c38d2
-于ec50b
-于22465
-于2504a
-于9f54d
-于e9f51
-于76d24
-于ad477
-于776a9
-于2833b
-于8141f
-于df36e
-于94300
-于1526d
-于d5eb5
-于cd528
-于ca3c1
-于9c1da
-于97812
-于7e3a9
-于2c08f
-于0d147
-于f2102
-于17f95
-于9caa4
-于d557c
-于1751d
-于1393c
-于0e171
-于6a478
-于71f3e
-于e8a78
-于77864
-于f5c2e
-于fb306
-于1e962
-于8d9ab
-于5c195
-于c72d0
-于05080
-于07860
-于895da
-于8cd1d
-于cf326
-于b09de
-于cb604
-于eeb3a
-于1481e
-于34f0d
-于6e997
-于86e84
-于fc86e
-于a1040
-于4c460
-于46ed0
-于3c405
-于a538f
-于2e738
-于00cf6
-于64857
-于25df5
-于1e87e
-于1cbe3
-于ab16f
-于291a9
-于4492a
-于f37cf
-于e18ac
-于7e583
-于329f0
-于f33fb
-于46988
-于58293
-于732fe
-于3e917
-于7488c
-于a2678
-于5a69f
-于169d2
-于dff53
-于e5efc
-于d87bf
-于c6ecb
-于e3851
-于2fbba
-于aee4d
-于a7527
-于f15d6
-于f41d9
-于73f0d
-于3cd4e
-于0edbb
-于bcdbd
-于a62e2
-于d2aa6
-于ca762
-于57193
-于cb4e6
-于ccb8e
-于58c98
-于e552e
-于97433
-于02974
-于3da3f
-于a5541
-于ae1d6
-于05423
-于9e901
-于3b009
-于65981
-于abaff
-于7eff8
-于20bc6
-于60772
-于02284
-于3cf38
-于5d470
-于21158
-于11a95
-于55b53
-于fbbf5
-于30881
-于26dad
-于97510
-于7ee75
-于f68ae
-于0253c
-于02a06
-于7bab5
-于7d50a
-于aa8e1
-于724fa
-于8c240
-于4e7e6
-于72969
-于1524d
-于5c6e0
-于6619d
-于c0bca
-于3c738
-于d70d4
-于6e9dc
-于ffbda
-于9785d
-于ca496
-于b648f
-于e3963
-于ef19f
-于7648c
-于efb0b
-于a2eed
-于79d30
-于ab01c
-于ffdc9
-于bb23b
-于32e2b
-于d2f64
-于cbb3a
-于610fe
-于f08aa
-于76989
-于86c50
-于027da
-于