5.内建数据结构

1 内建常用数据结构

  • 序列(sequence)
    • 字符串 str、字节序列 bytesbytearray
    • 列表 list、元组 tuple
  • 键值对(map)
    • 集合 set、字典 dict

序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放多个值的连续的内存空间。比如一个整数序列 [10, 20, 30, 40],可以这样示意表示:

images/5.内建数据结构/image-20220627202243435.png

由于 Python3 中一切皆对象,在内存中实际是按照如下方式存储的:

a = [10, 20, 30, 40]

images/5.内建数据结构/image-20220627202327485.png

从图示中,我们可以看出序列中存储的是整数对象的 * 地址*,而不是整数对象的值。

  • Python 中常用的序列结构有:字符串、列表、元组。
  • Python 中常用的键值对结构有:字典、集合。

上面学习的字符串就是一种序列。关于字符串里面很多操作,在序列中仍然会用到。

2 列表

2.1 为什么需要列表

思考
有一个人的姓名(TOM)怎么书写存储程序?

变量。
思考
如果一个班级 100 位学生,每个人的姓名都要存储,应该如何书写程序?声明 100 个变量吗?

No,我们使用列表就可以了,列表一次可以存储多个数据。
> 在 Python 中,我们把这种数据类型称之为列表。但是在其他的编程语言中,如 Java、PHP、Go 等等中其被称之为数组。
### 2.2 列表的定义

列表:用于存储任意数目、任意类型的数据集合。

列表的实现是一个顺序表,类似于 Java 或其它语言中的数组。

列表是内置可变序列,是包含多个元素的有序连续的内存空间。列表定义的标准语法格式:

列表序列名称 = [列表中的元素1, 列表中的元素2, 列表中的元素3, ...]

案例演示:定义一个列表,用于保存苹果、香蕉以及菠萝。

list1 = ['apple', 'banana', 'pineapple']
# list 列表类型支持直接打印
print(list1)
# 打印列表的数据类型
print(type(list1))
注意
列表可以一次存储多个数据且可以为不同的数据类型。
> > Python 的列表大小可变,根据需要随时增加或缩小。

字符串和列表都是序列类型,一个字符串是一个字符序列,一个列表是任何元素的序列。前面学习的很多字符串的方法,在列表中也有类似的用法,几乎一模一样。

列表

  • 一个排列整齐的队伍,Python:采用顺序表实现
  • 列表内的个体称作元素,由若干元素组成列表
  • 元素可以是任意对象(数字、字符串、对象、列表等)
  • 列表内元素有顺序,可以使用索引
  • 线性的数据结构
  • 使用 [] 表示
  • 列表是可变的

基本语法 [] 创建

a = []  # 创建一个空的列表对象

a = [10, 20, 'ym', 'fbb']

list() 创建

使用 list() 可以将任何可迭代的数据转化成列表。

#%%

# 创建一个空的列表对象
a = list()
a
#%%

# 字符串转列表
a = list("gaoqi,sxt")
a
#%%

# range 转列表
a = list(range(10))
a

images/5.内建数据结构/Snipaste_2022-06-28_14-01-35.png

range() 创建整数列表

range() 可以帮助我们非常方便的创建整数列表,这在开发中及其有用。语法格式为:

range([start,] end [,step])
参数
- start 参数:可选,表示起始数字。默认是 0。
> > - end 参数:必选,表示结尾数字。 > > - step 参数:可选,表示步长,默认为 1。
注意
Python3 中 `range()` 返回的是一个 **range 对象**,而不是列表。我们需要通过 `list()` 方法将其转换成列表对象。

典型示例如下:

#%%

# range() 返回一个range对象
type(range(1, 10))
#%%

list(range(3, 15, 2))
#%%

list(range(15, 3, -1))
#%%

list(range(3, -10, -1))

images/5.内建数据结构/Snipaste_2022-06-28_14-07-28.png

推导式生成列表

简介一下,重点在 for 循环后讲。

使用列表推导式可以非常方便的创建列表,在开发中经常使用。但是,由于涉及到 for 循环和 if 语句。在此,仅做基本介绍。在我们控制语句后面,会详细讲解更多列表推导式的细节。

#%%

#循环创建多个元素
a = [x * 2 for x in range(5)]
a
#%%

# 通过 if 过滤元素
a = [x * 2 for x in range(100) if x % 9 == 0]
a

images/5.内建数据结构/Snipaste_2022-06-28_14-10-48.png

2.3 列表的相关操作

列表的作用是一次性存储多个数据,程序员可以对这些数据进行的操作有:增、删、改、查

images/5.内建数据结构/Pasted-image-20250726000638.png

☆ 查操作

列表在计算机中的底层存储形式,列表和字符串一样,在计算机内存中都占用一段连续的内存地址,我们向访问列表中的每个元素,都可以通过索引下标的方式进行获取。

images/5.内建数据结构/image-20210310170752234.png

如果我们想获取列表中的某个元素,非常简单,直接使用索引下标:

list1 = ['apple', 'banana', 'pineapple']
# 获取列表中的 banana
print(list1[1])

images/5.内建数据结构/Snipaste_2022-06-28_14-16-25.png

注意
使用索引访问列表元素是列表最好的使用方式,复杂度是 O(1)。

查操作的相关方法:

函数 作用
index() 指定数据所在位置的下标,如果列表中没有指定数据,则报异常
count() 统计指定数据在当前列表中出现的次数,如果列表中不存在指定元素,返回 0
in 判断指定数据在某个列表序列,如果在返回 True,否则返回 False
not in 判断指定数据不在某个列表序列,如果不在返回 True,否则返回 False
len() 返回列表长度,即列表中包含元素的个数
☆ index()

index() 可以获取指定元素首次出现的索引位置。语法是:

index(value [,start [,end]])

startend 指定了搜索的范围。

#%%

# 1、查找某个元素在列表中出现的位置(索引下标)  
list1 = ['apple', 'banana', 'pineapple']  
print(list1.index('apple'))  # 0  
print(list1.index('peach'))  # 报错  
#%%  
  
a = [10, 20, 30, 40, 50, 20, 30, 20, 30]  
# 从头开始搜索列表中的第一个 20
print(a.index(20))  
# 从索引位置 3 开始往后搜索的第一个 20
print(a.index(20, 3))  
# 从索引位置 5 到 7 这个区间,第一次出现 30 元素的位置  
print(a.index(30, 5, 7))

images/5.内建数据结构/Snipaste_2022-06-28_14-25-22.png

images/5.内建数据结构/Snipaste_2022-06-28_14-25-33.png

☆ count()

count() 可以返回指定元素在列表中出现的次数。如果列表中不存在指定元素,返回 0。

举个栗子:

# 2、count() 方法:统计元素在列表中出现的次数
list2 = ['刘备', '关羽', '张飞', '关羽', '赵云']
# 统计一下关羽这个元素在列表中出现的次数
print(list2.count('关羽'))
print(list2.count('小龙女'))

images/5.内建数据结构/Snipaste_2022-06-28_15-09-26.png

☆ index 和 count 函数效率如何?

效率很差,因为他们都要遍历元素。

index 如果找的元素是列表的第一个元素,此时的时间复杂度最小为 O(1),但是最坏的情况当要找的元素位于最后时,时间复杂度为 O(n)。

count 需要遍历整个列表来进行计数,时间复杂度为 O(n)。

遍历元素都跟当前列表的数据的个数有关,元素多称为规模大

index 随着规模的增大而耗时增加,搜索效率随着规模增大而下降。

count 不管有多少元素,都需要完整遍历一遍,随着规模的增大而耗时增加。

性能极差,能不用则不用。在做算法题时,几乎不用。

使用索引访问列表元素是列表最好的使用方式,复杂度是 O(1)

注意
O(1) 不是说只要一步就能做完,而是指只需常数步就能完成,与规模 n 无关,不会随着规模 n 增加而影响效率。
☆ in && not in

判断列表中是否存在指定的元素,我们可以使用 count() 方法,返回 0 则表示不存在,返回大于 0 则表示存在。

但是,一般我们会使用更加简洁的 in 关键字来判断,直接返回 True 或 False。

in 效率高吗?

不高,因为需要遍历。和 index 函数一样,只是返回值不一样。

>>> # 本质是先比较地址是否一样,再比较内容
>>> [1] in [[1], [2]]
True
# 3、in 方法和 not in 方法(黑名单系统)
list3 = ['192.168.1.15', '10.1.1.100', '172.35.46.128']
if '10.1.1.100' in list3:
    print('黑名单IP,禁止访问')
else:
    print('正常IP,访问站点信息')

images/5.内建数据结构/Snipaste_2022-06-28_15-07-14.png

☆ len()

len() 返回列表长度,即列表中包含元素的个数。

#%%

a = [10, 20, 30]
len(a)

images/5.内建数据结构/Snipaste_2022-06-28_15-23-41.png

len 函数的时间复杂度为 O(1) 。

当创建一个列表时,列表内部会自动创建一个变量来记录列表长度。

☆ 增操作

当列表增加和删除元素时,列表会自动进行内存管理,大大减少了程序员的负担。但这个特点涉及列表元素的大量移动,效率较低。除非必要,我们一般只在列表的尾部添加元素 或删除元素,这会大大提高列表的操作效率。

函数 作用
append() 增加指定数据到列表中
extend() 列表结尾追加数据,如果数据是一个序列,则将这个序列的数据逐一添加到列表
insert() 指定位置新增数据
☆ append()

append():在列表的尾部追加元素。原地修改列表对象,是真正的列表尾部添加新的元素,速度最快,推荐使用。

append 方法没有返回值 或者说 返回值为 None,没有返回值往往表示它自己被改变了,就地修改。

思考
`append` 方法效率高吗?

不一定。
> > 定位尾部时间复杂度为 O(1)。 > > 一般情况下效率很高,但是当容器容量达到上限,需要扩容时,需要开辟一个新的内存空间,此时效率不高。 > > 虽然说列表是容器,但是有容量,python 中列表可以扩大,自动扩大,会先给初始大小,不够了扩容,初识扩容 4 倍,到一定程度时改为扩增 2 倍。扩容耗时,如果内存连续空间不够,牵扯到了垃圾回收,效率低了。
#%%

names = ['孙悟空', '唐僧', '猪八戒']
# 打印增加元素前的列表地址
print('增加元素前的列表地址', id(names))
# 在列表的尾部追加一个元素"沙僧"
names.append('沙僧')
# 打印增加元素后的列表地址
print('增加元素后的列表地址', id(names))
# 打印列表
print(names)

images/5.内建数据结构/Snipaste_2022-06-28_15-28-37.png

注意
列表追加数据的时候,直接在原列表里面追加了指定数据,即修改了原列表,故列表为可变类型数据。
☆ extend() 方法

列表结尾追加数据,如果数据是一个序列,则将这个序列的数据逐一添加到列表。属于原地操作,不创建新的列表对象。

思考
`extend` 方法效率高吗?

一般不涉及扩容时效率还行。当拼接的列表规模大时,效率不高。时间复杂度 O(n)。

list1.extend(Iterator) 等价于:

for i in iterator:
    list1.append()

案例:

#%%

list1 = ['Tom', 'Rose', 'Jack']
list2 = ['Hack', 'Jennify']

# 列表合并前的地址
print('列表合并后的地址', id(list1))

# 使用 extend 方法两个列表进行合并
list1.extend(list2)

# 列表合并后的地址
print('列表合并后的地址', id(list1))
print(list1)

images/5.内建数据结构/Snipaste_2022-06-28_15-32-44.png

总结
`extend` 方法比较适合于两个列表进行元素的合并操作。
l1 = [1, 2, 3]  
# extend 的参数必须为一个可迭代对象
l1.extend(1)  
  
print(l1)  

images/5.内建数据结构/Pasted-image-20250329165807.png

☆ insert() 方法

使用 insert() 方法可以将指定的元素插入到列表对象的任意指定位置。这样会让插入位置后面所有的元素进行移动,会影响处理速度。涉及大量元素时,尽量避免使用

类似发生这种移动的函数还有:remove()pop()del(),它们在删除非尾部元素时也会出现操作位置后面元素的移动。

insert (index, value)

作用:

在 index 的位置插入元素,如果右边界越界不报错,会将元素放到末尾;左边界小于 -(len - 1) 会将元素放到第一个位置。

names = ['薛宝钗', '林黛玉', '孙悟空', '猪八戒', '沙僧']  
# 在薛宝钗和林黛玉之间,插入一个新元素"贾宝玉"  
names.insert(1, '贾宝玉')  
print(names)  
  
names.insert(-2, '六耳猕猴')  
names.insert(-20, '玉帝')  
names.insert(20, '唐僧')  
print(names)

images/5.内建数据结构/Pasted-image-20250323174407.png

☆ + 运算符操作

向列表末尾追加元素,并不是真正的尾部添加元素,而是创建新的列表对象;将原列表的元素和新列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用。

思考
效率如何?

当列表元素不大时,还可以,但当数据规模大时效率极低。时间复杂度为 O(n)

本质上调用的是列表的魔术方法 __add()__ 方法。

#%%

l1 = [20, 30]
# 添加元素前
print('添加元素前', id(l1))
l1 = l1 + [50]
# 添加元素后
print('添加元素后', id(l1))

images/5.内建数据结构/Snipaste_2022-06-28_16-00-40.png

注意
`+` 和 `+=` 的效果不一样。
> > - 对于可变对象来说 `+=` 和 `+` 是不一样的(set 和 dict 没有这两个操作),可变对象 `+` 相当于对原对象 `+` 之后==**产生一个新对象**==。 > - 可变对象 `+=` 相当于修改原对象内部的值,并没有创建新对象,也就是==**修改前后对象的内存地址不变**==(id:内存地址的整数表示)。 > > ```python > #%% > > ls = [1, 2, 3] > print('列表原地址', id(ls)) > > ls1 = ls + [1, 2, 3] > print('+ 操作后地址', id(ls1)) > > ls += [1, 2, 3] > print('+= 操作后地址', id(ls)) > ``` > > ![images/5.内建数据结构/Snipaste_2022-06-28_16-05-53.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411015755496-377101459.png)
注意
但是对于不可变对象来说 `+=` 和 `+` 是一样的,都会产生一个新的对象。
> > 1. 原子对象(数值) > > ```python > #%% > > count = 0 > print(f'原地址:{id(count)},count的值:{count}') > > new_count = count + 1 > print(f'+ 操作后地址:{id(new_count)},new_count的值:{new_count}') > > count += 1 > print(f'+= 操作后地址:{id(count)},count的值:{count}') > ``` > > ![images/5.内建数据结构/Pasted-image-20250323175907.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411015755752-1763143988.png) > > 2. 元祖 > > ```python > #%% > > tu = (1, 2, 3) > print(f'原地址:{id(tu)},tu的值:{tu}') > > new_tu = tu + (1, 2, 3) > print(f'+ 操作后地址:{id(new_tu)},new_tu的值:{new_tu}') > > tu += (1, 2, 3) > print(f'+= 操作后地址:{id(tu)},tu的值:{tu}') > ``` > > ![images/5.内建数据结构/Pasted-image-20250323180634.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411015755485-1379856140.png)
☆ 乘法扩展

使用乘法扩展列表,生成一个新列表,新列表元素是原列表元素的多次重复。

#%%

a = ['小龙女',100]
b = a * 3

print(a)
print(b)

images/5.内建数据结构/Snipaste_2022-06-28_16-14-21.png

适用于乘法操作的,还有:字符串、元组。例如:

#%%

c = '小龙女'
d = c * 3

print(c)
print(d)

images/5.内建数据结构/Snipaste_2022-06-28_16-15-47.png

☆ 删操作

函数 作用
del 列表[索引] 删除列表中的某个元素
pop() 删除指定下标的数据(默认为最后一个),并返回该数据
remove() 移除列表中某个数据的第一个匹配项。如果不存在,抛异常
clear() 清空列表,删除列表中的所有元素,返回空列表。
☆ del 删除指定的列表元素

基本语法:

names = ['Tom', 'Rose', 'Jack', 'Jennify']
# 删除Rose
del names[1]
# 打印列表
print(names)

images/5.内建数据结构/Snipaste_2022-06-28_16-17-09.png

删除列表指定位置的元素:

#%%

a = [100,200,888,300,400]
del a[2]
a

images/5.内建数据结构/Snipaste_2022-06-28_16-20-22.png

images/5.内建数据结构/image-20220628161856511.png

☆ pop() 方法

作用:删除指定下标的元素,如果不填写下标,默认删除最后一个。其返回结果:就是删除的这个元素。

思考
效率高吗?

> - 指定索引,在中间弹出元素,效率不高。移除元素会涉及后面元素的移动和复制,效率不高。不推荐。 > > - 不指定索引,默认弹出最后一个元素,效率高。推荐这么使用。
注意
> 以后,遇到了 `pop` 这样的名字,都可以叫做弹出来,弹出返回值。
#%%

names = ['貂蝉', '吕布', '董卓']
del_name = names.pop()

print(del_name)
print(names)
#%%

names = ['貂蝉', '吕布', '董卓']
del_name = names.pop(1)

print(del_name)
print(names)

images/5.内建数据结构/Snipaste_2022-06-28_16-22-37.png

l1 = [1, 2, 3]  
# 报索引越界异常
print(l1.pop(9))

images/5.内建数据结构/Pasted-image-20250329170134.png

☆ remove() 方法

作用:删除首次出现的指定元素,若不存在该元素抛出异常。

思考
效率高吗?

效率很差。按值找元素要通过遍历,效率不高,而且当元素在中间时,会涉及后面元素的移动和复制,效率极其差。尽量不用。
#%%

fruit = ['apple', 'banana', 'pineapple', 'banana']
fruit.remove('banana')
print(fruit)
#%%

fruit = ['apple', 'banana', 'pineapple', 'banana']
# 列表中没有该元素,抛异常
fruit.remove('peach')
print(fruit)

images/5.内建数据结构/Snipaste_2022-06-28_16-25-10.png

☆ clear() 方法

清空列表,只是清空列表中的所有元素,列表本身并没有被删除。

思考
效率高吗?

高,标记不用了,直接将 `length` 置为 0。
> > 但是 `clear` 方法要慎用。内寸是宝贵的,内存中好不容易算的结果放在列表,说不要就要了?
#%%

names = ['貂蝉', '吕布', '董卓']
# 随着故事的发展,人物都 game over
names.clear()
# 打印列表
print(names)

images/5.内建数据结构/Snipaste_2022-06-28_16-26-52.png

☆ 改操作

函数 作用
列表[索引] = 修改后的值 修改列表中的某个元素
reverse() 将数据序列进行倒叙排列
sort() 对列表序列进行排序
copy() 对列表序列进行拷贝
☆ 索引修改列表内容
list1 = ['貂蝉', '大乔', '小乔', '八戒']  
# 修改列表中的元素  
list1[3] = '周瑜'  
print(list1)  
  
list2 = [1, 6, 5, 9, 1, 3]  
list2.reverse()  
print("列表倒序排列:", list2)  
  
list3 = [10, 50, 20, 30, 1]  
list3.sort()  # 升序(从小到大)  
print("列表升序排列", list3)  
# 或  
list3.sort(reverse=True)  # 降序(从大到小)  
print("列表降序排列", list3)  
  
list4 = list3.copy()  
print("列表复制", list4)

images/5.内建数据结构/Pasted-image-20250323184931.png

通过索引修改列表效率非常高,时间复杂度为 O(1)。

☆sort()

修改原列表,不建新列表的排序

对列表序列进行排序,默认是升序排序。

sort(key, reverse=False)
  • key:排序函数,传递一个高阶函数。

    l1 = [1, 2, 'a', 7]
    l1.sort(key=str)  # 类似于 if str(2) > str('a')
    print(l1)
    

images/5.内建数据结构/Pasted-image-20250323185031.png

  • reverse:False 升序排列,True 降序排列,默认为 False。
#%%

a = [20, 10, 30, 40]

# 默认是升序排列
a.sort()
print(a)
# 降序排列
a.sort(reverse=True)
print(a)
# 打乱顺序
import random

random.shuffle(a)
print(a)

images/5.内建数据结构/Snipaste_2022-06-28_17-45-16.png

☆ sorted()

建新列表的排序。

我们也可以通过内置函数 sorted() 进行排序,这个方法返回新列表,不对原列表做修改。

#%%

a = [20, 10, 30, 40]

print('原列表地址', id(a))
# 默认升序排列
b = sorted(a)
print('升序排列列表地址', id(b))
print(b)
# 降序排列
c = sorted(a, reverse=True)
print('降序排列列表地址', id(c))
print(c)

images/5.内建数据结构/Snipaste_2022-06-28_17-52-46.png

通过上面操作,我们可以看出,生成的列表对象 b 和 c 都是完全新的列表对象。

☆ reversed()

返回迭代器。

内置函数 reversed() 也支持进行逆序排列,与列表对象 reverse() 方法不同的是,内置函数 reversed() 不对原列表做任何修改,只是返回一个逆序排列的迭代器对象。

#%%

a = [20, 10, 30, 40]  
  
b = reversed(a)  
  
print(b)  
print(list(b))

images/5.内建数据结构/Pasted-image-20250323185430.png

我们打印输出 c 发现提示是:list_reverseiterator。也就是一个迭代对象。

同时,我们使用 list(c) 进行输出,发现只能使用一次。第一次输出了元素,第二次为空。那是因为迭代对象在第一次时已经遍历结束了,第二次不能再使用。

注意
关于迭代对象的使用,后续章节会进行详细讲解。

☆ 列表相关的其他内置函数汇总

copy()

复制一个新的列表,是浅复制。

#%%

l1 = [1, 2, 'str', [1, 2]]
l2 = l1.copy()
print(id(l1))
print(id(l2))
#%%

print('修改前:')
print(l1)
print(l2)
l2[3][0] = 100
print('修改后:')
print(l1)
print(l2)

images/5.内建数据结构/Snipaste_2022-07-13_20-50-09.png

max 和 min

用于返回列表中最大和最小值。

#%%

a = [3, 10, 20, 15, 9]

print('最大值', max(a))
print('最小值', min(a))

images/5.内建数据结构/Snipaste_2022-06-28_18-44-19.png

sum

对数值型列表的所有元素进行求和操作,对非数值型列表运算则会报错。

#%%

a = [3, 10, 20, 15, 9]
sum(a)
#%%

b = ['xcz', 10, 'dd']
sum(b)

images/5.内建数据结构/Snipaste_2022-06-28_18-46-54.png

☆ 切片操作

我们在前面学习字符串时,学习过字符串的切片操作,对于列表的切片操作和字符串类似。切片是 Python 序列及其重要的操作,适用于列表、元组、字符串等等。切片 slice 操作可以让我们快速提取子列表或修改。切片的格式如下:

 [起始偏移量 start: 终止偏移量 end [:步长 step]]

:当步长省略时顺便可以省略第二个冒号。

  • 典型操作(三个量为正数的情况)如下:
操作和说明 示例 结果
[:] 提取整个列表 [10, 20, 30][:] [10,20,30]
[start:] 从 start 索引开始到结尾 [10, 20, 30][1:] [20,30]
[:end] 从头开始直到 end-1 [10, 20, 30][:2] [10,20]
[start: end] 从 start 到 end-1 [10, 20, 30, 40][1: 3] [20,30]
[start: end :step] 从 start 提取到 end-1,步长是 step [10, 20, 30, 40, 50, 60, 70][1: 6: 2] [20, 40, 60]
  • 其他操作(三个量为负数)的情况:
说明 示例 结果
倒数三个 [10, 20, 30, 40, 50, 60, 70][-3:] [50,60,70]
倒数第五个到倒数第三个(包左不包右) [10, 20, 30, 40, 50, 60, 70][-5: -3] [30,40]
步长为负,从右到左反向提取 [10, 20, 30, 40, 50, 60, 70][::-1] [70, 60, 50, 40, 30, 20, 10]

切片操作时,起始偏移量和终止偏移量不在 [-(长度), 长度 - 1] 这个范围,也不会报错。起始偏移量小于 -(长度) 则会当做 0,终止偏移量大于 长度 - 1 会被当成 长度 - 1

例如:

#%%

# 正常输出了结果,没有报错
[10, 20, 30, 40][1: 30]

images/5.内建数据结构/Snipaste_2022-06-28_15-19-38.png

☆ 通过索引直接访问元素

我们可以通过索引直接访问元素。索引的区间在 [-列表长度, 列表长度 - 1] 这个范围。超过这个范围则会抛出异常。

#%%

a = [10, 20, 30, 40, 50, 20, 30, 20, 30]

print(a[2])
print(a[10])

images/5.内建数据结构/Snipaste_2022-06-28_16-34-03.png

2.4 列表的循环遍历

思考
什么是循环遍历?

循环遍历就是使用 while 或 for 循环对列表中的每个数据进行打印输出。

while 循环:

list1 = ['貂蝉', '大乔', '小乔']

# 定义计数器
i = 0
# 编写循环条件
while i < len(list1):
    print(list1[i])
    # 更新计数器
    i += 1

images/5.内建数据结构/Snipaste_2022-06-28_17-34-07.png

for 循环(个人比较推荐):

list1 = ['貂蝉', '大乔', '小乔']
for i in list1:
    print(i)

images/5.内建数据结构/Snipaste_2022-06-28_17-34-33.png

2.5 列表的嵌套

列表的嵌套:列表中又有一个列表,我们把这种情况就称之为列表嵌套。

> 在其他编程语言中,称之为叫做二维数组或多维数组。
一维列表可以帮助我们存储一维、线性的数据。

二维列表可以帮助我们存储二维、表格的数据。例如下表的数据:

姓名 年龄 薪资 城市
小龙女 18 30000 北京
黄蓉 19 20000 上海
赵敏 20 10000 深圳
#%%

a = [
    ["小龙女", 18, 30000, "北京"],
    ["黄蓉", 19, 20000, "上海"],
    ["赵敏", 20, 10000, "深圳"],
    ]
print(a[1][0], a[1][1], a[1][2])

images/5.内建数据结构/image-20220628190724257.png

嵌套循环打印二维列表所有的数据:

#%%

a = [
    ["小龙女", 18, 30000, "北京"],
    ["黄蓉", 19, 20000, "上海"],
    ["赵敏", 20, 10000, "深圳"],
    ]

for i in a:
    for j in i:
        print(j, end=" ")
    print()

images/5.内建数据结构/Snipaste_2022-06-28_19-10-35.png

3 元组

3.1 为什么需要元组

思考
如果想要存储多个数据,但是这些数据是不能修改的数据,怎么做?

列表?列表可以一次性存储多个数据,但是列表中的数据允许更改。
num_list = [10, 20, 30]
num_list[0] = 100

那这种情况下,我们想要存储多个数据且数据不允许更改,应该怎么办呢?

答:使用元组,元组可以存储多个数据且元组内的数据是不能修改的

列表属于可变序列,可以任意修改列表中的元素。元组属于不可变序列,不能修改元组中的元素。因此,元组没有增加元素、修改元素、删除元素相关的方法。

所以,我们只需要学习元组的创建和删除,元组中元素的访问和计数即可。元组支持如下操作:

  1. 索引访问
  2. 切片操作
  3. 连接操作
  4. 成员关系操作
  5. 比较运算操作
  6. 计数:元组长度 len()、最大值 max()、最小值 min()、求和 sum() 等。

3.2 元组的定义

元组特点:定义元组使用小括号,且使用逗号隔开各个数据,数据可以是不同的数据类型

通过 () 创建元组

通过 () 创建元组,小括号可以省略。

#%%

# 创建一个空元组
a = ()
type(a)
#%%

a = (10, 20, 30)
a
#%%

# 可以省略括号
a = 10, 20, 30
a

images/5.内建数据结构/image-20220714144202158.png

如果元组只有一个元素,则必须后面加逗号。这是因为解释器会把 (1) 解释为整数 1,(1, ) 解释为元组。

#%%

a = (1)
print(type(a))

a = (1,)
print(type(a))

# 可省略括号
a = 1,
print(type(a))

images/5.内建数据结构/Snipaste_2022-06-28_19-21-01.png

通过 tuple() 创建元组

tuple(可迭代的对象)
#%%

# 创建一个空元组对象
b = tuple()
print(b)

b = tuple("abc")
print(b)

b = tuple(range(3))
print(b)

b = tuple([2,3,4])
print(b)

images/5.内建数据结构/Snipaste_2022-06-28_19-24-32.png

元组优化问题

#%%

a = tuple()
b = ()
a is b

images/5.内建数据结构/image-20220714145415115.png

#%%

a = tuple((1, ))
b = (1, )
a is b

images/5.内建数据结构/image-20220714145620398.png

注意
Python 对空元组进行了优化,默认在内存中**只会创建一个空元组对象**,即空元组的引用地址是相同的;但是除了空元组以外,创建内容相同的两个元组,都是在内存中创建了不同的对象。
总结
> `tuple()` 可以接收列表、字符串、其他序列类型、迭代器等生成元组。 > > `list()` 可以接收元组、字符串、其他序列类型、迭代器等生成列表。

3.3 元组的相关操作方法

由于元组中的数据不允许直接修改,所以其操作方法大部分为查询方法。

函数 作用
元组[索引] 根据索引下标查找元素
index() 查找某个数据,如果数据存在返回对应的下标,否则报错,语法和列表、字符串的 index 方法相同
count() 统计某个数据在当前元组出现的次数
len() 统计元组中数据的个数

案例 1:修改元组中的某个元素。元组不可以修改,如果修改元组的值会抛出异常。

#%%

a = (20, 10, 30, 9, 8)
a[2] = 33

images/5.内建数据结构/Snipaste_2022-06-28_19-29-35.png

案例 2:访问元组中的某个元素。

#%%

a = (20, 10, 30, 9, 8)
print(a[1])
print(a[1: 3])
print(a[:4])

images/5.内建数据结构/Snipaste_2022-06-28_19-31-49.png

案例 3:查找某个元素在元组中出现的位置,存在则返回索引下标,不存在则直接报错。

nums = (10, 20, 30)
print(nums.index(20))

images/5.内建数据结构/Snipaste_2022-06-28_19-37-01.png

案例 4:统计某个元素在元组中出现的次数。

nums = (10, 20, 30, 50, 30)
print(nums.count(30))

images/5.内建数据结构/image-20220628193743122.png

案例 5:len() 方法主要就是求数据序列的长度,字符串、列表、元组。

nums = (10, 20, 30, 50, 30)
print(len(nums))

images/5.内建数据结构/Snipaste_2022-06-28_19-38-08.png

列表关于排序的方法 list.sort() 是修改原列表对象,元组没有该方法。如果要对元组排序,只能使用内置函数 sorted(tupleObj),并生成新的列表对象。

#%%

a = (20, 10, 30, 9, 8)
b = sorted(a)
print(b)

images/5.内建数据结构/Snipaste_2022-06-28_19-40-09.png

3.4 zip

zip(列表 1,列表 2,…) 将多个列表对应位置的元素组合成为元组,并返回这个 zip 对象。

#%%

a = [10, 20, 30]
b = [40, 50, 60, 99]
c = [70, 80, 90]
d = zip(a, b, c)
print(d)
print(list(d))

images/5.内建数据结构/Snipaste_2022-06-28_19-43-51.png

压缩后的元组个数等于最短的列表的长度。

3.5 生成器推导式创建元组

从形式上看,生成器推导式与列表推导式类似,只是生成器推导式使用小括号。列表推导式直接生成列表对象,生成器推导式生成的不是列表也不是元组,而是一个生成器对象

我们可以将生成器对象,转化成列表或者元组。也可以使用生成器对象的 __next__() 方法进行遍历,或者直接作为迭代器对象来使用。但不管什么方式使用,元素访问结束后,如果需要重新访问其中的元素,必须重新创建该生成器对象。

#%%

s = (i for i in range(5))
s
#%%

print(tuple(s))
#只能访问一次元素。第二次就为空了。需要再生成一次
print(list(s))
#%%

s = (x*2 for x in range(5))
print(s.__next__())
print(s.__next__())
print(s.__next__())
print(s.__next__())
print(s.__next__())
# 已经没有元素,再打印,抛出异常
print(s.__next__())

images/5.内建数据结构/Snipaste_2022-06-28_20-17-15.png

images/5.内建数据结构/Snipaste_2022-06-28_20-17-23.png

3.6 元组总结

  1. 元组的核心特点是:不可变序列。
  2. 元组的访问和处理速度比列表快。
  3. 与整数和字符串一样,元组可以作为字典的键,列表则永远不能作为字典的键使用。

4 字典

4.1 为什么需要字典

思考 1:比如我们要存储一个人的信息,姓名:Tom,年龄:20 周岁,性别:男,如何快速存储。

person = ['Tom', '20', '男']

思考 2:在日常生活中,姓名、年龄以及性别同属于一个人的基本特征。但是如果使用列表对其进行存储,则分散为 3 个元素,这显然不合逻辑。我们有没有办法,将其保存在同一个元素中,姓名、年龄以及性别都作为这个元素的 3 个属性。

答:使用 Python 中的字典。

4.2 Python 中字典 (dict) 的概念

字典是键值对无序可变结构,字典中的每个元素都是一个键值对,包含:键对象值对象。可以通过键对象实现快速获取、删除、更新对应的值对象

列表中我们通过下标数字找到对应的对象。字典中通过键对象找到对应的值对象

是任意的不可变数据,比如:整数、浮点数、字符串、元组。

列表、字典、集合这些可变对象,不能作为。并且键不可重复

可以是任意的数据,并且可重复

特点:

  1. 符号为大括号(花括号) => {}
  2. 数据为键值对形式出现 => {key:value},key:键名,value:值,在同一个字典中,key 必须是唯一(类似于索引下标)
  3. 各个键值对之间用逗号隔开

定义:

# 有数据字典
dict1 = {'name': 'Tom', 'age': 20, 'gender': '男'}

# 空字典
dict2 = {}

dict3 = dict()

4.3 字典的创建

  1. 我们可以通过 {}dict() 来创建字典对象。

    #%%
    
    a = {'name': 'Tom', 'age': 18, 'job': 'programmer'}  
    
    # 使用 name=value 对初始化一个字典  
    b = dict(name='Tom', age=18, job='programmer')  
    # 使用可迭代对象的二元组  
    c = dict([["name", "Tom"], ("age", 18)])  
    
    d = {}  # 空的字典对象  
    e = dict()  # 空的字典对象  
    
    print("a", a)  
    print("b", b)  
    print("c", c)  
    print("d", d)  
    print("e", e)
    

    images/5.内建数据结构/Pasted-image-20250323193812.png

    dict(**kwargs) 使用 key=value 键值对对初始化一个字典。

    dict(iterable, **kwarg) 使用可迭代对象和 key=value 对构造字典,不过可迭代对象的元素必须是一个二元结构。

    dict(mapping, **kwarg) 使用一个字典构建另一个字典。

    #%%
    
    # 使用 name=value 对初始化一个字典  
    a = dict(name='Tom', age=18, job='programmer')
    
    # 使用可迭代对象和 name=value 对构造字典  
    b = dict([("name", "Tom"), ("age", 18)], job='programmer')
    
    # 使用一个字典构建另一个字典  
    c = dict(b, address='Beijing')
    
    print("a", a)  
    print("b", b)  
    print("c", c)
    

    images/5.内建数据结构/Pasted-image-20250323194552.png

    #%%
    
    d1 = {'name': '小龙女', 'age': 18}
    
    # 使用一个字典构建另一个字典
    d2 = dict(d1, salary = 2000)
    print(d2)
    

    images/5.内建数据结构/image-20220920212255819.png

    #%%
    
    dict([('name', 'jack'), ('age', 18)])
    

    images/5.内建数据结构/image-20220920210541270.png

    #%%
    
    dict([('name', 'jack'), ('age', 18)], [('salary', 2000), ('address', 'BeiJing')])
    

    images/5.内建数据结构/image-20220920210734713.png

    #%%
    
    dict([('name', 'jack'), ('age', 18)], salary = 2000, address='BeiJing')
    

    images/5.内建数据结构/image-20220920211009304.png

    #%%
    
    dict(name = '小龙女', [('age', 17)])
    

    images/5.内建数据结构/image-20220920211136524.png

  2. 通过 zip() 创建字典对象。

    #%%
    
    k = ['name', 'age', 'job', 'a']
    v = ['gaoqi', 18, 'techer']
    d = dict(zip(k, v))
    d
    

    images/5.内建数据结构/Snipaste_2022-06-28_20-32-23.png

  3. 通过 fromkeys(iterable, value) 创建值为空的字典。

    • iterable:可迭代对象,将可迭代对象中的每一个元素作为字典的键。
    • value:缺省值,如果没有设置 value 缺省值,默认将值设为 None。
    #%%
    
    k = ['name','age','job', 'a']
    
    # 无缺省值 value
    d = dict.fromkeys(k)
    d
    

    images/5.内建数据结构/Snipaste_2022-06-28_20-34-10.png

    #%%
    
    k = ['name', 'age', 'job', 'a']
    
    # 设置缺省值 value
    d = dict.fromkeys(k, '我是缺省值')
    d
    

    images/5.内建数据结构/image-20220920213059712.png

4.4 字典的增操作

☆ 给字典新增“键值对”

基本语法:

字典名称[key] = value
注意
如果 key 存在则修改这个 key 对应的值;如果 key 不存在则新增此键值对。

案例:定义一个空字典,然后添加 nameage 以及 address 这样的 3 个 key。

#%%

# 1、定义一个空字典
person = {}
# 2、向字典中添加数据
person['name'] = '小龙女'
person['age'] = 18
person['address'] = '活死人墓'
# 3、使用print方法打印person字典
print(person)
注意
列表、字典为可变类型,不能做键对象

images/5.内建数据结构/image-20220628224915882.png

4.5 字典的删操作

☆ del 字典名称 [key]

删除指定元素。

# 1、定义一个有数据的字典
person = {'name': '王大锤', 'age': 28, 'gender': 'male', 'address': '北京市海淀区'}
# 2、删除字典中的某个元素(如 gender)
del person['gender']
# 3、打印字典
print(person)

images/5.内建数据结构/Pasted-image-20250323195711.png

☆ clear() 方法

清空字典中的所有 key。

# 1、定义一个有数据的字典
person = {'name':'王大锤', 'age':28, 'gender':'male', 'address':'北京市海淀区'}  
print(person)
# 2、使用 clear() 方法清空字典
person.clear()
# 3、打印字典
print(person)

images/5.内建数据结构/Pasted-image-20250323195844.png

☆ pop() 方法

pop() 删除指定键值对,并返回对应的“值对象”。

a = {'name': '小龙女', 'age': 18, 'job': 'programmer'}  
b = a.pop('job')  
print(a)  
print(b)

images/5.内建数据结构/Pasted-image-20250323200048.png

☆ popitem() 方法

随机删除和返回该键值对。字典是无序可变序列,因此没有第一个元素、最后一个元素的概念;popitem 弹出随机的项,因为字典并没有最后的元素或者其他有关顺序的概念。若想一个接一个地移除并处理项,这个方法就非常有效(因为不用首先获取键的列表)。

#%%

a = {'name':'小龙女', 'age':18, 'job':'programmer'}

while len(a) > 0:
    print(a.popitem())

images/5.内建数据结构/Snipaste_2022-06-29_14-30-12.png

4.6 字典的改操作

☆ 修改键值对

基本语法:

字典名称[key] = value
如果 key 存在则修改这个 key 对应的值;如果 key 不存在则新增此键值对。

案例:定义一个字典,里面有 nameage 以及 address,修改 address 这个 key 的 value 值。

# 1、定义字典  
person = {'name': '孙悟空', 'age': 600, 'address': '花果山'}  
# 2、修改字典中的数据(address)  
person['address'] = '东土大唐'  
# 3、打印字典  
print(person)

images/5.内建数据结构/Pasted-image-20250323200542.png

☆ update()

使用 update() 将新字典中所有键值对全部添加到旧字典对象上。如果 key 有重复,则直接覆盖。

#%%

a = {'name': '小龙女', 'age': 18, 'job': 'programmer'}
b = {'name': '赵敏', 'money': 1000, 'sex': '女'}
a.update(b)
a

images/5.内建数据结构/Snipaste_2022-06-29_14-13-53.png

4.7 字典的查操作

① 查询方法:使用具体的某个 key 查询数据,如果未找到,则直接报错。

字典序列[key]
#%%

a = {'name':'gaoqi','age':18,'job':'programmer'}
print(a['name'])
# 键不存在,抛出异常
print(a['sex'])

images/5.内建数据结构/Snipaste_2022-06-28_22-15-49.png

② 字典的相关查询方法

函数 作用
get(key[, default]) 根据字典的 key 获取对应的 value 值,如果当前查找的 key 不存在则返回第二个参数(默认值),如果省略第二个参数,则返回 None
setdefault(key[, default]) 返回 key 对应 的值 value
key 不存在,添加 kw 对,value 设置为 default,并返回 default,如果 default 没有设置,缺省为 None
keys() 以列表返回一个字典所有的键
values() 以列表返回字典中的所有值
items() 以列表返回可遍历的 (键, 值) 元组

☆ get()

通过 get() 方法获得。推荐使用。

优点是:指定键不存在,返回 None;也可以设定指定键不存在时默认返回的对象。推荐使用 get() 获取值对象

案例 1:使用 get 获取字典中某个 key 的 value 值。

# 1、定义一个字典  
cat = {'name': 'Tom', 'age': 5, 'address': '美国纽约'}  
# 2、获取字典的相关信息  
name = cat.get('name')  
age = cat.get('age')  
gender = cat.get('gender', 'male')  # get(key, 默认值)  
address = cat.get('address')  
print(f'姓名:{name},年龄:{age},性别:{gender},住址:{address}')

images/5.内建数据结构/Pasted-image-20250323201412.png

☆ setdefault(key, value)

setdefault(k, value) 方法用于设置 key 的默认值。

该方法接收两个参数,第 1 个参数表示 key,第 2 个参数 value 表示默认值。

如果 key 在字典中不存在,那么 setdefault() 方法会向字典中添加这个 key,并用第 2 个参数作为 key 的值。该方法会返回这个默认值。

如果未指定第 2 个参数,那么 key 的默认值是 None。如果字典中已经存在这个 key,setdefault() 不会修改 key 原来的值,而且该方法会返回 key 原来的值。

#%%

# 定义一个空字典
dict = {}

# 向字典中添加一个名为name的key,默认值是小龙女,输出结果:小龙女
print(dict.setdefault('name', '小龙女'))
# 输出结果:{'name': '小龙女'}
print(dict)

# 并没有改变name的值,输出结果:小龙女
print(dict.setdefault('name', '黄蓉'))
# 输出结果:{'name': '小龙女'}
print(dict)

# 向字典中添加一个名为age的key,默认值是None,输出结果:None
print(dict.setdefault('age'))
# 输出结果:{'name': '小龙女', 'age': None}
print(dict)

images/5.内建数据结构/image-20220920231218050.png

☆ keys()

返回字典中的所有的键对象。

案例 2:提取 person 字典中的所有 key。

# 1、定义一个字典  
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}  
# 2、提取字典中的name、age以及mobile属性  
print(type(person.keys()))  
print(person.keys())

images/5.内建数据结构/Pasted-image-20250323201945.png

☆ values()

返回字典中的所有值对象。

案例 3:提取 person 字典中的所有 value 值。

# 1、定义一个字典  
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}  
# 2、提取字典中的貂蝉、18以及13765022249号码  
print(type(person.values()))  
print(person.values())

images/5.内建数据结构/Pasted-image-20250323202101.png

☆ items()

返回可遍历的 (键, 值) 元组列表。

案例 4:使用 items() 方法提取数据。

# 1、定义一个字典  
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 2、调用items方法获取数据,dict_items([('name', '貂蝉'), ('age', 18), ('mobile', '13765022249')])  
print(type(person.items()))
print(person.items())
# 3、结合for循环对字典中的数据进行遍历  
for key, value in person.items():
    print(f'{key}:{value}')

images/5.内建数据结构/Pasted-image-20250323202507.png

注:

Python3 中,keys、values、items 方法返回一个类似一个生成器的可迭代对象,但是可以重复使用。

  • Dictionary view 对象,可以使用 len()iter()in 操作
  • 字典的 entry 的动态的视图,字典变化,视图将反映出这些变化
  • keys 返回一个类 set 对象,也就是可以看做一个 set 集合。如果 values 都可以 hash,那么 items 也可以看做是类 set 对象
  • value 可以是任意合法值,不能保证唯一,也不一定能 hash,values() 方法返回可迭代对象,不是类 set 对象。

Python2 中,上面的方法会返回一个新的列表,立即占据新的内存空间。所以 Python 建议使用 iterkeys、itervalues、iteritems 版本,返回一个迭代器,而不是返回一个 copy。

#%%

d1 = dict([("name", "小龙女"), ("age", 18), ("salary", 2000)], address="活死人墓")
# %%

print(type(d1.keys()))
print(type(d1.values()))
print(type(d1.items()))
# %%

# 创建字典视图
keys = d1.keys()
values = d1.values()
items = d1.items()

print("字典更新前keys:", keys)
print("字典更新前values:", values)
print("字典更新前items:", items)
print("==========================")

d1.update({"门派": "古墓派"}, cp="杨过", age=19)

# 字典的视图不会立即返回结果,可以展示字典发生的变化
print("字典更新后keys:", keys)
print("字典更新后values:", values)
print("字典更新后items:", items)

images/5.内建数据结构/image-20221008195705124.png

images/5.内建数据结构/image-20221008195718639.png

☆ len()

返回字典中键值对的个数。

#%%

person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
len(person)

images/5.内建数据结构/Snipaste_2022-06-28_22-22-40.png

☆ in

默认检测一个是否在字典中。

#%%

person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 默认检测键是否在字典中
print('name' in person)
# 检测值是否在字典中
print('貂蝉' in person.values())

images/5.内建数据结构/Snipaste_2022-06-28_22-28-23.png

4.8 字典的“有序性”

字典元素是按照 key 的 hash 值无序存储的

但是,有时候我们却需要一个有序的元素顺序,Python3.6 之前,使用 OrderedDict 类可以做到,3.6 开始 dict 自身支持。到底 Python 对一个无序数据结构记录了什么顺序?

注意:有序指的是记住了输入序,并不是排序。

images/5.内建数据结构/image-20221008203701658.png

images/5.内建数据结构/image-20221008203715995.png

Python3.6 之前,在不同的机器上,甚至同一个程序分别运行 2 次,都不能确定不同的 key 的先后顺序。

images/5.内建数据结构/image-20221008203759382.png

images/5.内建数据结构/image-20221008203817605.png

Python3.6+,记录了字典 key 的录入顺序,遍历的时候,就是按照这个顺序。

如果使用 d = {'a': 300, 'b': 20, 'c': 10, 'd': 50},就会造成以为字典按照 key 排序的错觉。

目前,建议不要 3.6 提供的这种字典特性,还是认为字典返回的是无序的。如果非要这种有序,建议使用 OrderedDict。

4.9 序列解包

序列解包可以用于元组、列表、字典。序列解包可以让我们方便的对多个变量赋值。

x, y, z = (20, 30, 10)
print(f"x:{x}")
print(f"y:{y}")
print(f"z:{z}")
print("-" * 20)

(a, b, c) = (9, 8, 10)
print(f"a:{a}")
print(f"b:{b}")
print(f"c:{c}")
print("-" * 20)

[d, e, f] = [10, 20, 30]
print(f"d:{d}")
print(f"e:{e}")
print(f"f:{f}")
print("-" * 20)

images/5.内建数据结构/Pasted-image-20250926162423.png

序列解包用于字典时,默认是对进行操作;如果需要对键值对操作,则需要使用 items();如果需要对进行操作,则需要使用 values()

s = {'name': 'gaoqi', 'age': 18, 'job': 'teacher'}
# 默认对键进行操作
name, age, job = s
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)

# 对键值对进行操作
name, age, job = s.items()
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)

# 对值进行操作
name, age, job = s.values()
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)

images/5.内建数据结构/Pasted-image-20250926162716.png

4.10 封装和解构

基本概念

t1 = 1, 2
print(type(t1))  # 什么类型

t2 = (1, 2)
print(type(t2))

images/5.内建数据结构/Pasted-image-20250926162745.png

Python 等式右侧出现逗号分隔的多值的时候,就会将这几个值封装到元组中。这种操作称为封装 (packing)

x, y = (1, 2)

print(f"x:{x}")  # 1
print(f"y:{y}")  # 2

images/5.内建数据结构/Pasted-image-20250926162854.png

Python 中等式右侧是一个容器类型,左侧是逗号分隔的多个标识符,则将右侧容器中数据和左侧标识符一一对应。这种操作称为解构(unpacking)

从 Python3 开始,对解构做了很大的改进,现在用起来已经非常的方便快捷。

封装和解构是非常方便的提取数据的方法,在 Python、JavaScripts 等语言中应用极广。

# 交换数据
x = 4
y = 5
temp = x
x = y
y = temp

# 封装和解构,交换
x = 10
y = 11
x, y = y, x

简单解构

# 左右个数相同
a, b = 1, 2
a, b = (1, 2)
a, b = [1, 2]
a, b = [10, 20]
a, b = {10, 20}  # 非线性结构
a, b = {'a': 10, 'b': 20}  # 非线性结构也可以解构

[a, b] = (1, 2)
[a, b] = 10, 20
(a, b) = {30, 40}

等号左边只能是线性数据结构,等号右边可以是线性数据结构或非线性数据结构。左边的小括号或中括号可以省略不写,在嵌套使解构时用。

那么,左右个数不一致可以吗?

a, b = (10, 20, 30)

images/5.内建数据结构/Pasted-image-20250926163152.png

注意
**左右个数一定要一一对应。**

例:如何将下列所有数子都解构出来?

(1, [2, 3, 4], 5)

下面这种写法显然不对,左右个数不一致。

a, b, c, d, e = (1, [2, 3, 4], 5)

print(a, b, c, d, e)

images/5.内建数据结构/Pasted-image-20250926163230.png

正确写法:

a, [b, c, d], e = (1, [2, 3, 4], 5)

print(a, b, c, d, e)

images/5.内建数据结构/Pasted-image-20250926165217.png

剩余变量解构

在 Python3.0 中增加了剩余变量解构(rest)。

a, *rest, b = [1, 2, 3, 4, 5]

print(a, b)
print(type(rest), rest)  # <class 'list'>[2,3,4]

images/5.内建数据结构/Pasted-image-20250926170117.png

标识符 rest 将尽可能收集剩余的数据组成一个列表。

a, *rest = [1, 2, 3, 4, 5]
print(a, rest)

a, *rest, b = [1, 2, 3, 4, 5]
print(a, rest, b)  # 内容是什么?

*rest, b = [1, 2, 3, 4, 5]
print(rest, b)  # 内容是什么?

images/5.内建数据结构/Pasted-image-20250926170254.png

*rest = [1, 2, 3, 4, 5]
print(rest)  # 内容是什么?

images/5.内建数据结构/Pasted-image-20250926170416.png

注意
**剩余变量不可以单独使用。**
a, *r1, *r2, b = [1, 2, 3, 4, 5]  # ?

images/5.内建数据结构/Pasted-image-20250926170835.png

注意
**剩余变量不能多次使用,只能使用一次。**
a, *_, b = [1, 2, 3, 4, 5]

print(_)

images/5.内建数据结构/Pasted-image-20250926171320.png

_, *b, _ = [1, 2, 3]

print(_)  # 第一个是什么
print(b)  # 是什么
print(_)  # 第二个是什么

images/5.内建数据结构/Pasted-image-20250926171729.png

在 IPython 中实验,_ 是最后一个输出值,这里将把它覆盖。

_ 是合法的标识符,这里它没有什么可读性,它在这里的作用就是表示不关心这个变量的值,我不想要。有人把它称作丢弃变量。

4.11 字典核心底层原理 (重要)

字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket。

每个 bucket 有两部分:一个是键对象的引用,一个是值对象的引用。

由于所有 bucket 结构和大小一致,我们可以通过偏移量来读取指定 bucket。

images/5.内建数据结构/image-20220629150547825.png

将一个键值对放进字典的底层过程

a = {}
a["name"]="小龙女"

假设字典 a 对象创建完后,数组长度为 8:

images/5.内建数据结构/image-20220629151736374.png

我们要把 name=”gaoqi” 这个键值对放到字典对象 a 中,首先第一步需要计算键 name 的散列值。Python 中可以通过 hash() 来计算。

>>> bin(hash("name"))
'-0b1010111101001110110101100100101'

由于数组长度为 8,我们可以拿计算出的散列值的最右边 3 位数字作为偏移量,即“101”,十进制是数字 5。我们查看偏移量 5,对应的 bucket 是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边 3 位作为偏移量,即“100”,十进制是数字 4。再查看偏移量为 4 的 bucket 是否为空。直到找到为空的 bucket 将键值对放进去。流程图如下:

images/5.内建数据结构/image-20220629154453924.png

images/5.内建数据结构/image-20220629152740405.png

扩容

Python 会根据散列表的拥挤程度扩容。

扩容指的是:创造更大的数组,将原有内容拷贝到新数组中。接近 2/3 时,数组就会扩容。

根据键查找“键值对”的底层过程

我们明白了,一个键值对是如何存储到数组中的,根据键对象取到值对象,理解起来就简单了。

>>> a.get("name")
'gaoqi'

当我们调用 a.get("name"),就是根据键 name 查找到“键值对”,从而找到值对象 gaoqi

第一步,我们仍然要计算 name 对象的散列值:

>>> bin(hash("name"))
'-0b1010111101001110110101100100101'

和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。假设数组长度为 8,我们可以拿计算出的散列值的最右边 3 位数字作为偏移量,即“101”,十进制是数字 5。我们查看偏移量 5,对应的 bucket 是否为空。如果为空,则返回 None。如果不为空,则将这个 bucket 的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回 None。流程图如下:

images/5.内建数据结构/image-20220629155457563.png

用法总结

  1. 键必须可散列。

    1. 数字、字符串、元组,都是可散列的。
    2. 自定义对象需要支持下面三点:
      • 支持 hash() 函数。
      • 支持通过 __eq__ () 方法检测相等性。
      • a == b 为真,则 hash(a) == hash(b) 也为真。
  2. 字典在内存中开销巨大,典型的空间换时间。

  3. 键查询速度很快。

  4. 往字典里面添加新键可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字典的同时进行字典的修改。

4.12 综合案例:通讯录管理系统

需求:开个一个通讯录的管理系统,主要用于实现存储班级中同学的信息(姓名、年龄、电话)。

知识点:列表、字典、死循环

延伸:在 Python 中,我们可以使用字典来保存一个人的基本信息。但是如果想保存多个人的信息,我们必须把列表和字典结合起来。

students = [0, 1, 2]
student = {'name': '刘备', 'age': 18, 'mobile': '10086'}

# 组装:

students = [
    {'name': '刘备', 'age': 18, 'mobile': '10086'},
    {'name': '关羽', 'age': 17, 'mobile': '10000'},
    {'name': '张飞', 'age': 16, 'mobile': '10010'}
]

为什么需要死循环:

当我们选中某个菜单时,功能一旦执行完毕,则整个程序就执行结束了。为了保存程序可以一直运行下去,可以模拟死循环的效果,让程序一直运行下去。

while True:
    ...

要求:正常情况下,通讯录管理系统应该有 4 个功能:增删改查。

# 1、定义一个列表,将来用于存储所有学员的通讯信息
students = []

# 2、打印功能菜单
print('-' * 40)
print('欢迎使用传智教育通讯录管理系统V1.0')
print('[1] 增加学员信息')
print('[2] 删除学员信息')
print('[3] 退出系统')
print('-' * 40)

while True:
    # 3、提示用户进行相关操作
    user_num = int(input('请输入您要进行的操作编号:'))

    if user_num == 1:
        # 4、提示用户输入学员的信息
        student = {}
        student['name'] = input('请输入学员的姓名:')
        student['age'] = int(input('请输入学员的年龄:'))
        student['mobile'] = input('请输入学员的电话:')
        # 5、把学员信息保存在列表中
        students.append(student)
        print(students)

    elif user_num == 2:
        name = input('请输入要删除的学员信息:')
        # 6、遍历所有学员信息
        for i in students:
            if i['name'] == name:
                # 从列表中删除整个学员(字典)
                students.remove(i)
                print('删除成功')
                print(students)
            else:
                print('您要删除的学员信息不存在')

    elif user_num == 3:
        print('感谢您使用传智教育通讯录管理系统V1.0')
        break

    else:
        print('输入错误,请重新输入要操作的编号')

5 集合

5.1 什么是集合

集合是无序可变序列,元素不能重复。

实际上,集合底层是字典实现集合的所有元素都是字典中的键对象,因此是不能重复且唯一的

集合(set)是一个无序的不重复元素序列。

  • 天生去重
  • 无序

5.2 集合的定义

在 Python 中,我们可以使用一对花括号 {} 或者 set() 方法来定义集合,但是如果你定义的集合是一个空集合,则只能使用 set() 方法。

使用 set(),将列表、元组等可迭代对象转成集合。如果原来数据存在重复数据,则只保留一个。

# 定义一个集合
s1 = {10, 20, 30, 40, 50}
print(s1)
print(type(s1))

# 定义一个集合:集合中存在相同的数据
s2 = {'刘备', '曹操', '孙权', '曹操'}
print(s2)
print(type(s2))
print('################')

# 使用 set() 方法将可迭代对象转换成集合
# 如果原来数据存在重复数据,则只保留一个。
l1 = [1, 2, 3, 3, 5, 6, 6]
tu = 1, 2, 3, 3, 5, 6, 6
s3 = set(l1)
s4 = set(tu)
print(s3)
print(s4)
print('################')

# 定义空集合
s5 = {}
s6 = set()
print(type(s5))  # <class 'dict'>
print(type(s6))  # <class 'set'>

images/5.内建数据结构/Pasted-image-20250926173419.png

5.3 集合操作的相关方法(增删查)

☆ 集合的增操作

  1. add() 方法:向集合中增加一个元素(单一)。

    students = set()
    
    students.add('小龙女')
    students.add('赵敏')
    
    print(students)
    

    images/5.内建数据结构/Pasted-image-20250926173917.png

  2. update() 方法:向集合中增加可迭代类型的数据(字符串、列表、元组、字典)。

    students = set()
    list1 = ['刘备', '关羽', '赵云']
    
    students.update(list1)
    
    print(students)
    

    images/5.内建数据结构/Pasted-image-20250926174011.png

students = set()

students.add('刘德华')
students.add('黎明')

# 使用 update 新增元素
students.update('蔡徐坤')

print(students)

images/5.内建数据结构/Pasted-image-20250926174053.png

☆ 集合的删操作

  1. remove() 方法:删除集合中的指定数据,如果数据不存在则报错。
  2. discard() 方法:删除集合中的指定数据,如果数据不存在也不会报错。
  3. pop() 方法:随机删除集合中的某个数据,并返回这个数据。
思考
效率如何?

`remove` 方法使用 key 移除元素,因为集合是通过 hash 定位,不用像列表遍历元素,所以寻找元素的效率极高是 O(1)。`discard` 同理。
# 1、定义一个集合
products = {'萝卜', '白菜', '水蜜桃', '奥利奥', '西红柿', '凤梨'}

# 2、使用 remove 方法删除白菜这个元素
products.remove('白菜')
print(products)

# 3、使用 discard 方法删除未知元素
products.discard('玉米')
print(products)

# 4、使用 pop 方法随机删除某个元素
del_product = products.pop()
print(del_product)

# 2、使用 remove 方法删除不存在元素
products.remove('小龙女')

images/5.内建数据结构/Pasted-image-20250926174901.png

☆ 集合中的查操作

  1. in:判断某个元素是否在集合中,如果在,则返回 True,否则返回 False。
  2. not in:判断某个元素不在集合中,如果不在,则返回 True,否则返回 False。
思考
效率如何?

高,和 `remove` 的原理一样。通过 hash 值来定位元素,效率极高,不需要像 list 一样遍历。效率为 O(1)。
# 定义一个set集合
s1 = {'刘帅', '英标', '高源'}

# 判断刘帅是否在s1集合中
if '刘帅' in s1:
    print('刘帅在s1集合中')
else:
    print('刘帅没有出现在s1集合中')

images/5.内建数据结构/Pasted-image-20250926175506.png

  1. 集合的遍历操作
for i in 集合:
    print(i)

5.4 集合中的交集、并集与差集特性

在 Python 中,我们可以使用 & 来求两个集合的交集:

images/5.内建数据结构/image-20210312152451774.png

在 Python 中,我们可以使用 | 来求两个集合的并集:

images/5.内建数据结构/image-20210312152631528.png

在 Python 中,我们可以使用 - 来求两个集合的差集:

images/5.内建数据结构/image-20210312152921040.png

# 求集合中的交集、并集、差集
s1 = {'刘备', '关羽', '张飞', '貂蝉'}
s2 = {'袁绍', '吕布', '曹操', '貂蝉'}

# 求两个集合中的交集
print(s1 & s2)

# 求两个集合中的并集
print(s1 | s2)

# 求连个集合中的差集
print(s1 - s2)
print(s2 - s1)

images/5.内建数据结构/Pasted-image-20250926175657.png

6 序列中的公共方法

6.1 什么是公共方法

所谓的公共方法就是支持操作大部分数据序列的方法。

6.2 常见公共方法 1

运算符 描述 支持的容器类型
+ 合并 字符串、列表、元组
* 复制 字符串、列表、元组
in 元素是否存在 字符串、列表、元组、字典、集合
not in 元素是否不存在 字符串、列表、元组、字典、集合

案例 1:+ 合并。

# 1、+ 加号,代表两个序列之间的连接与整合
str1 = 'hello'
str2 = 'world'
print(str1 + str2)

# 2、定义两个列表,对其数据进行整合
list1 = ['刘备', '关羽']
list2 = ['诸葛亮', '赵云']
print(list1 + list2)

# 3、定义两个元组,对其数据进行整合
tuple1 = (10, 20)
tuple2 = (30, 40)
print(tuple1 + tuple2)

images/5.内建数据结构/Pasted-image-20250926175850.png

案例 2:* 复制。

# 1、字符串与乘号的关系
print('-' * 40)
print('传智教育Python管理系统V1.0')
print('-' * 40)

# 2、列表与乘号的关系
list1 = ['*']
print(list1 * 10)

# 3、元组与乘号的关系
tuple1 = (10,)
print(tuple1 * 10)

images/5.内建数据结构/Pasted-image-20250926175931.png

案例 3:innot in 方法。

ips = ['192.168.10.11', '10.1.1.100', '172.15.184.31']

if '10.1.1.100' in ips:
    print('列表中元素已存在')
else:
    print('列表中元素不存在')

images/5.内建数据结构/Pasted-image-20250926180022.png

6.3 常见公共方法 2

函数 描述
len() 计算容器中元素个数
deldel() 根据索引下标删除指定元素
max() 返回容器中元素最大值
min() 返回容器中元素最小值
range(start, end, step) 生成从 start 到 end(不包含)的数字,步长为 step,供 for 循环使用
enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。

案例 1:len() 获取字符串、列表、元组、字典、集合的长度。

# 定义一个字符串
str1 = 'hello world'
print(f'字符串的长度为{len(str1)}')

# 定义一个列表
list1 = [10, 20, 30, 40, 50]
print(f'列表的长度为{len(list1)}')

# 定义一个字典
dict1 = {'name': '哆啦A梦', 'gender': 'male', 'address': '东京'}
print(f'字典的长度为{len(dict1)}')

images/5.内建数据结构/Pasted-image-20250926180156.png

案例 2:del 方法,用于删除序列中指定的元素(根据索引下标)。

# 定义一个列表
list1 = ['吕布', '董卓', '貂蝉']
# 使用 del 方法删除董卓
del list1[1]
print(list1)

# 定义一个字典
dict1 = {'name': '白龙马', 'age': 23, 'address': '东海龙宫'}
# 使用 del 方法删除 age
del dict1['age']
print(dict1)
del (dict1['name'])
print(dict1)

images/5.内建数据结构/Pasted-image-20250926180237.png

案例 3:求某个序列中元素的最大值和最小值。

num1 = int(input('请输入第一个数:'))
num2 = int(input('请输入第二个数:'))
num3 = int(input('请输入第三个数:'))

list1 = [num1, num2, num3]
max_num = max(list1)
min_num = min(list1)

print(f'最大值:{max_num}')
print(f'最小值:{min_num}')
num1 = int(input('请输入第一个数:'))
num2 = int(input('请输入第二个数:'))
num3 = int(input('请输入第三个数:'))

list1 = [num1, num2, num3]
max_num = max(list1)
min_num = min(list1)

print(f'最大值:{max_num}')
print(f'最小值:{min_num}')

images/5.内建数据结构/Pasted-image-20250926180421.png

案例 4:enumerate(),把一个序列类型中的每个元素构造成 (index, value) 结构,然后结合 for 循环进行遍历。

list1 = [10, 20, 30, 40, 50]
n = 1
for i in list1:
    print(f'第{n}个数:{i}')
    n += 1

print('-' * 40)

for key, value in enumerate(list1):
    print(f'第{key + 1}个数:{value}')

images/5.内建数据结构/Pasted-image-20250926180928.png

6.4 序列类型之间的相互转换

list() 方法:把某个序列类型的数据转化为列表。

# 1、定义元组类型的序列
tuple1 = (10, 20, 30)
print(list(tuple1))

# 2、定义一个集合类型的序列
set1 = {'a', 'b', 'c', 'd'}
print(list(set1))

# 3、定义一个字典
dict1 = {'name': '刘备', 'age': 18, 'address': '蜀中'}
print(list(dict1))

images/5.内建数据结构/Pasted-image-20250926181050.png

tuple() 方法:把某个序列类型的数据转化为元组。

# 定义一个列表类型的数据
list1 = ['a', 'b', 'c', 'd']
print(tuple(list1))

# 定义一个集合类型的数据
set1 = {10, 20, 30, 40}
print(tuple(set1))

images/5.内建数据结构/Pasted-image-20250926181123.png

set() 方法:将某个序列转换成集合(但是要注意两件事 => ① 集合可以快速完成列表去重 ② 集合不支持下标)。

# 定义一个列表类型的数据
list1 = ['a', 'b', 'c', 'd', 'a']
print(set(list1))

# 定义一个元组类型的数据
tuple1 = (10, 20, 30, 40)
print(set(tuple1))

images/5.内建数据结构/Pasted-image-20250926181209.png

7 元组的不可变与字典键对象的不可变

元组的不可变和字典键对象的不可变一样吗?

tu = ('a', 1, [1, 2, 3])
print(tu)

images/5.内建数据结构/Pasted-image-20250926181259.png

dict = {'a': 1, 1: 2, [1, 2, 3]: 3}

images/5.内建数据结构/Pasted-image-20250926181340.png

dict = {'a': 1, 1: 2, ([1, 2, 3]): 3}

images/5.内建数据结构/Pasted-image-20250926181401.png

同样是要求不可变,但是元组可以使用列表,而字典却不可以。

这是因为元组的不可变是指的索引(即变量在内存的地址)不可变,而字典的键对象要求的不可变是 hash 值 的不可变

详细讲解请看 hash 详解:[[hash详解]]

8 各个数据结构比较

线性数据结构(有序,可索引):listtuplestr

非线性数据结构(无序,不可索引):dictset

如果内存中有线程列表,你需要做的操作,里面用到了遍历,时间复杂度为 O(n),n 规模越大,效率越低下。微秒 => 毫秒。如 indexcount 方法。

如果内存中有集合,不管数据规模多大,它的检索元素的时间,不随着规模变化,时间复杂度为 O(1),效率极高。ns(纳秒)级别。如 keyin

posted @ 2026-04-11 01:57  挖掘鱼  阅读(7)  评论(0)    收藏  举报