
🌟 开场白:别让数据结构成为你的拦路虎!
各位Pythoner,大家好!我是你们的老朋友花姐💁♀️。
说起Python的数据结构,很多人第一反应是啥?------"这还不简单?" 是的,Python的数据结构确实比C++那种指针乱飞的世界友好太多,但是真正用起来,很多人又容易踩坑,甚至有些高级用法直接被忽略了。
比如,你知道列表(list)在Python底层是如何动态扩容的吗?
你知道字典(dict)的查找为什么那么快?
你知道元组(tuple)真的不可变吗?
今天,咱们就来一场深度解析!
一、列表(List):Python最灵活的数据结构!
1. 列表的基本用法
列表(list
)是Python中最常见的数据结构,它支持存储多个元素 ,并且可以包含不同类型的数据。
1.1 创建列表
python
# 创建一个普通列表
fruits = ["苹果", "香蕉", "橙子"]
# 创建一个空列表
empty_list = []
# 创建一个包含不同数据类型的列表
mixed_list = [1, "hello", 3.14, True]
1.2 访问和修改列表元素
python
fruits = ["苹果", "香蕉", "橙子"]
print(fruits[0]) # 输出: 苹果
print(fruits[-1]) # 输出: 橙子(支持负索引)
fruits[1] = "梨" # 修改元素
print(fruits) # 输出: ['苹果', '梨', '橙子']
1.3 添加元素
python
fruits.append("葡萄") # 在列表末尾追加
fruits.insert(1, "西瓜") # 在指定索引插入元素
print(fruits)
1.4 删除元素
python
fruits.remove("橙子") # 按值删除
del fruits[0] # 按索引删除
print(fruits)
1.5 遍历列表
python
for fruit in fruits:
print(fruit)
2. 列表的底层原理:动态数组
Python的 list
其实就是动态数组 ,类似C语言的 malloc
,它会预留一定的空间,存放数据。如果超出了容量,Python会自动扩容,避免频繁分配内存的性能损耗。
2.1 Python列表是如何扩容的?
python
import sys
lst = []
print(f"初始列表大小: {sys.getsizeof(lst)} 字节")
for i in range(10):
lst.append(i)
print(f"添加 {i} 后,列表大小: {sys.getsizeof(lst)} 字节")
💡 总结:
- Python的
list
采用动态数组存储元素。 - 自动扩容 机制提高性能,但如果知道最终大小,可以用
lst = [None] * n
预分配。
二、字典(Dict):Python的查找神器!
1. 字典的基本用法
字典(dict
)是一种键值对(key-value)存储的数据结构,类似现实中的"联系人列表"👇:
python
# 创建字典
person = {
"name": "花姐",
"age": 28,
"city": "北京"
}
# 访问字典的值
print(person["name"]) # 输出: 花姐
# 添加/修改键值
person["gender"] = "女"
person["age"] = 29
print(person)
# 删除键值
del person["city"]
print(person)
# 遍历字典
for key, value in person.items():
print(f"{key}: {value}")
2. 字典的查找为什么快?
字典的查询速度比列表快很多 ,因为字典是基于哈希表(Hash Table)实现的。来看下面的对比👇:
python
import time
# 用列表查找
lst = list(range(1000000))
start = time.time()
999999 in lst # 判断是否存在
print(f"列表查找耗时: {time.time() - start:.6f} 秒")
# 用字典查找
dct = {i: None for i in range(1000000)}
start = time.time()
999999 in dct
print(f"字典查找耗时: {time.time() - start:.6f} 秒")
📌 结果 :字典查找远快于列表,因为字典的键存储在哈希表中,查找时直接通过哈希计算位置,而列表需要逐个扫描(O(n))。
三、元组(Tuple):真的不可变吗?
1. 元组的基本用法
元组(tuple
)和列表很像,但它不可变,一旦创建就无法修改。
python
# 创建元组
t = (1, 2, 3)
print(t[0]) # 访问元素
# 不能修改元素(会报错)
# t[0] = 100 #❌ TypeError: 'tuple' object does not support item assignment
# 但可以包含可变对象
t = (1, 2, [3, 4])
t[2].append(5) # 居然成功了?
print(t) # (1, 2, [3, 4, 5])
🚀 为什么可变了?
其实,元组本身确实是不可变的 ,但如果元组里存的是可变对象(如列表),那么这个可变对象的内容依然可以被修改!
💡 总结:
- 元组的不可变性 指的是元组的结构不可变,但元组内部的可变对象仍然可以改变。
- 如果要创建真正的不可变数据结构 ,可以使用
collections.namedtuple
或dataclasses.dataclass(frozen=True)
。
2. Python中的不可变数据结构:namedtuple
和 dataclass(frozen=True)
🌟 为什么要用不可变的数据结构?
在 Python 里,默认的数据结构(如 list
、dict
、普通的 tuple
)通常是可变的,这意味着我们可以在程序运行时随意更改它们的内容。但在某些场景下,我们希望数据是只读的,比如:
- 保护数据不被意外修改,防止 bug。
- 让代码更具有可读性,明确表明某些数据不应更改。
- 让数据结构可作为字典的
key
(可哈希)。
2.1 namedtuple
:轻量级的不可变数据结构
Python 的 collections.namedtuple
提供了一种类似 tuple
但带有字段名称 的不可变数据结构。它的底层仍然是 tuple
,但可以使用字段名来访问数据,而不是只能用索引,非常方便!
2.1.1 基本使用
python
from collections import namedtuple
# 定义一个不可变的 Point 结构体
Point = namedtuple("Point", ["x", "y"])
# 创建 Point 实例
p = Point(3, 5)
print(p.x) # 3
print(p.y) # 5
print(p) # Point(x=3, y=5)
💡 为什么比普通 tuple
好?
python
# 使用普通的 tuple
p1 = (3, 5)
print(p1[0]) # 3
print(p1[1]) # 5
# 用索引访问,不直观,容易出错
namedtuple
的最大优势就是可读性更好 !比起 tuple[0]
,point.x
一眼就能看懂它是什么数据。
2.1.2 namedtuple
是不可变的
既然是不可变数据结构,那么修改字段会报错👇:
python
p.x = 10 # ❌ AttributeError: can't set attribute
如果你想要一个类似的结构,但允许修改,可以使用 dataclass
(后面会讲)。
2.1.3 namedtuple
的额外功能
2.1.3.1 ._asdict()
:转换为字典
python
p = Point(3, 5)
print(p._asdict())
# 输出:{'x': 3, 'y': 5}
2.1.3.2 ._replace()
:创建新实例
python
new_p = p._replace(x=10)
print(new_p) # Point(x=10, y=5)
注意:_replace()
不会修改原来的对象,而是返回一个新的对象。
2.1.3.3 额外默认值
python
Person = namedtuple("Person", ["name", "age", "city"])
p = Person._make(["花姐", 28, "北京"])
print(p) # Person(name='花姐', age=28, city='北京')
2.2 dataclass(frozen=True)
:更强大的不可变数据结构
namedtuple
已经很不错了,但它还是有一些缺点,比如:
- 不能设置默认值。
- 不能使用类型注解。
- 不能定义方法。
为了解决这些问题,Python 3.7 引入了 dataclasses
,其中 frozen=True
可以创建真正不可变的数据结构!
2.2.1 基本使用
python
from dataclasses import dataclass
@dataclass(frozen=True) # 冻结 dataclass,使其不可变
class Point:
x: int
y: int
p = Point(3, 5)
print(p.x) # 3
print(p.y) # 5
跟 namedtuple
一样,dataclass(frozen=True)
也会阻止修改:
python
p.x = 10 # ❌ dataclasses.FrozenInstanceError: cannot assign to field 'x'
⚠️ 注意 :不同于 namedtuple
,dataclass(frozen=True)
底层是 __slots__
+ __dict__
,而不是 tuple
。
2.2.2 dataclass(frozen=True)
的优势
✅ 支持默认值
python
@dataclass(frozen=True)
class Person:
name: str
age: int = 18 # 默认值
p = Person("花姐")
print(p) # Person(name='花姐', age=18)
相比 namedtuple
,dataclass
可以直接给 age
赋默认值,而 namedtuple
需要用 _replace()
这种不太优雅的方法。
✅ 支持方法
namedtuple
只能存储数据,而 dataclass
还能添加方法👇:
python
@dataclass(frozen=True)
class Rectangle:
width: int
height: int
def area(self) -> int:
return self.width * self.height
r = Rectangle(10, 5)
print(r.area()) # 50
✅ 自动实现 __eq__
、__repr__
python
@dataclass(frozen=True) # 冻结 dataclass,使其不可变
class Point:
x: int
y: int
p1 = Point(3, 5)
p2 = Point(3, 5)
print(p1 == p2) # ✅ True,dataclass 自动实现 __eq__
print(p1) # ✅ Point(x=3, y=5),自动实现 __repr__
而 namedtuple
也可以自动实现 __eq__
,但 dataclass
允许更多定制,比如:
python
@dataclass(frozen=True)
class Point:
x: int
y: int
def __repr__(self):
return f"坐标({self.x}, {self.y})"
p = Point(3, 5)
print(p) # 坐标(3, 5)
2.3 namedtuple
vs. dataclass(frozen=True)
:如何选择?
特性 | namedtuple |
dataclass(frozen=True) |
---|---|---|
不可变 | ✅ | ✅ |
底层结构 | tuple |
__slots__ + __dict__ |
支持字段默认值 | ❌ | ✅ |
支持类型注解 | ❌ | ✅ |
支持方法 | ❌ | ✅ |
可读性 | 普通 | 更优 |
👉 什么时候用 namedtuple
?
- 仅仅是存储少量数据,不需要方法和默认值。
- 需要快速创建轻量级不可变对象。
👉 什么时候用 dataclass(frozen=True)
?
- 需要方法、类型注解、默认值。
- 需要更好的可读性和可维护性。
🎉 总结:牢记这些关键点!
今天,我们深度解析了Python的三大数据结构:列表、字典、元组 ,并且剖析了它们的底层原理。最重要的几个要点: ✅ 列表(list) :可变 、支持动态扩容,适用于存储有序数据。
✅ 字典(dict) :键值对存储 、查找快,适用于存储映射关系。
✅ 元组(tuple) :不可变(但内部可变对象仍然可变),适用于存储固定数据。
希望今天的讲解能让你更深入理解Python的数据结构!💪
顺手点赞+在看就是对花姐最大的支持! ❤️🎉