Python 数据类型 - 序列与容器
什么是序列?什么是容器
序列(Sequence)
核心特征:
- 有"顺序"
- 可以被索引(通常支持
__getitem__) - 可以被迭代
- 通常有长度(
__len__)
典型序列:
| 类型 | 有序 | 可变 |
|---|---|---|
str |
✅ | ❌ |
list |
✅ | ✅ |
tuple |
✅ | ❌ |
📌 关键认知:
"有序 + 可索引" 才是序列的本质
容器(Container)
核心特征:
- 用来"装对象"
- 支持成员判断:
x in container - 不一定有顺序
- 不一定可索引
典型容器:
| 类型 | 有序 | 可变 |
|---|---|---|
list |
✅ | ✅ |
tuple |
✅ | ❌ |
set |
❌ | ✅ |
dict |
❌(逻辑) | ✅ |
📌 容器 ≠ 序列
set、dict是容器,但不是序列
统一视角:Python 容器装的是什么?
Python 的容器存的不是"值",而是"对象的引用"。
python
a = 10
lst = [a]
内存模型是:
lst ─▶ [ ● ]
│
▼
10
📌 这会直接影响:
- 可变 / 不可变
- 浅拷贝 / 深拷贝
- 参数传递
- 多容器共享数据
逐个拆解核心类型(从原理到行为)
str ------ 不可变字符序列
本质
-
Unicode 字符序列
-
不可变对象
-
是序列,不是容器(偏语义)
s = "hello"
不可变的真正含义
不可变指的是一旦改变某个变量的值,那么该变量的内存地址也跟着改变。
s[0] = "H" # ❌ TypeError
但:
s = s + "!"
📌 不是修改原字符串
而是:
- 创建新字符串对象
- 绑定新名字
为什么 str 要不可变?
- 可 hash(能作为 dict key)
- 安全(多线程、缓存)
- 性能(intern、共享)
list ------ 可变、顺序容器
本质
-
动态数组(array-like)
-
存放 对象引用
-
可变
lst = [1, 2, 3]
可变体现在哪里?
lst.append(4)
lst[0] = 100
📌 list 的"可变"是指:
容器结构可以变化,而不是元素本身
list 的一个"误区"
a = [1, 2]
b = a
b.append(3)
结果:
a == [1, 2, 3]
📌 原因:
a和b指向同一个 list 对象
tuple ------ 不可变的序列容器
本质
-
固定长度
-
不可变结构
-
但内部元素可能是可变对象
t = (1, [2, 3])
t[1].append(4) # ✅
📌 不可变的是"容器结构",不是"内容递归冻结"
tuple 为什么存在?
- 可作为 dict key
- 比 list 轻量
- 表达"语义上的不可变数据"
set ------ 无序、去重、哈希容器
本质
-
基于 hash table
-
元素必须 可 hash
-
无序(不能索引)
s = {1, 2, 3}
为什么不能有 list?
{[1, 2]} # ❌ TypeError
📌 因为:
- list 是可变的
- 可变对象 hash 不稳定
set 的核心用途
- 去重
- 集合运算(并、交、差)
- 高效成员判断(O(1))
dict ------ 键值映射容器(Python 的核心)
本质
-
hash table
-
key → value 映射
-
key 必须可 hash
d = {"a": 1, "b": 2}
dict 的核心特性
- 键是唯一的
- 插入顺序在 Python 3.7+ 保留(实现保证)
📌 注意:
dict 是"逻辑无序",不是"实现无序"
dict 的执行模型视角
d[k] = v
等价逻辑:
hash(k)- 定位桶
- 比较
== - 存储引用
is VS ==
a = [1, 2]
b = [1, 2]
a == b # True
a is b # False
==:值相等(内容)is:同一对象(id 相同)
📌 容器比较默认是 递归内容比较
id()
id(obj)
- 返回对象在当前解释器中的唯一标识
- 通常是内存地址(CPython)
📌 用途:
- 调试引用关系
- 理解浅 / 深拷贝
- 理解参数传递
小结
Python 中:
- 一切皆对象
- 容器存的是引用
- 变量是名字
- 可变性决定行为
- hash 决定能否做 key / set 元素
进阶--------------------------------------------------------------------
Python 可变 (Mutable) VS 不可变 (Immutable) 对象
可变 vs 不可变,和"变量"没关系,和"对象"有关
- ❌ 错误理解:变量能不能改
- ✅ 正确理解:对象创建后,其"内部状态"能否改变
Python 世界的统一前提:一切皆对象。
创建一个变量并赋值,实际上是将一个对象绑定到一个变量名字上。
不可变对象:对象一旦创建,其内部状态不能再被修改。
典型不可变类型:
intfloatboolstrtuplefrozenset
可变对象:对象创建后,其内部结构或内容可以被修改。
Python 浅拷贝 vs 深拷贝(copy / deepcopy)
核心不是"拷没拷",而是"拷贝到哪一层"。
a = [1, 2]
真实内存结构(逻辑图):
a ─────▶ [ ● , ● ]
│ │
▼ ▼
1 2
当容器里再装容器:
a = [1, [2, 3]]
a ─────▶ [ ● , ● ]
│ │
▼ ▼
1 [2, 3]
📌 拷贝的复杂性,从从容器嵌套开始
浅拷贝:只复制"最外层容器",内部对象仍然共享 。创建一个新的容器对象,但容器内部的元素引用原对象。
深拷贝:递归复制所有可变对象,直到不可变为止 。递归复制对象及其引用的所有可变子对象。
最直观的浅拷贝方式
python
import copy
a = [1, [2, 3]]
b = copy.copy(a)
内存关系:
a ─────▶ [ ● , ● ]
│ │
▼ ▼
1 [2, 3]
b ─────▶ [ ● , ● ]
│ │
▼ ▼
1 [2, 3] ← 共享
📌 外壳不同,内部共享
修改外层 vs 修改内层
b.append(4)
-
a不变 -
b变b[1].append(99)
-
a和b一起变
📌 这就是浅拷贝的本质后果
常见"隐式浅拷贝"方式
| 写法 | 类型 |
|---|---|
a[:] |
list 浅拷贝 |
list(a) |
浅拷贝 |
dict(a) |
浅拷贝 |
set(a) |
浅拷贝 |
a.copy() |
浅拷贝 |
📌 它们都只拷贝一层
深拷贝
基本用法
python
import copy
a = [1, [2, 3]]
b = copy.deepcopy(a)
内存关系:
a ─────▶ [ ● , ● ]
│ │
▼ ▼
1 [2, 3]
b ─────▶ [ ● , ● ]
│ │
▼ ▼
1 [2, 3] ← 新对象
📌 彻底隔离
修改验证
python
b[1].append(99)
a不变b改变
一个"非常容易误判"的例子
python
a = [1, 2, 3]
b = copy.deepcopy(a)
你可能觉得这行代码"很重",但实际上:
📌 对纯不可变元素来说:
deepcopy≈copy- 因为
int不可变,不会真的复制
拷贝 ≠ 等价
python
a = [1, [2]]
b = copy.copy(a)
c = copy.deepcopy(a)
a == b == c # True
a is b # False
a[1] is b[1] # True
a[1] is c[1] # False
📌 判断拷贝效果,永远用 is
dict / set 的拷贝本质一样
dict 示例
a = {"x": [1, 2]}
b = a.copy()
a ─▶ { "x": ● }
│
▼
[1, 2]
b ─▶ { "x": ● } ← 共享
set 示例
python
a = {1, 2, (3, 4)}
b = copy.copy(a)
- tuple 不可变,安全
- 若元素是可变对象(不允许)
深拷贝的"代价"和风险
1️⃣ 性能问题
- 递归复制
- 容器深度越大,成本越高
📌 不要"无脑 deepcopy"
2️⃣ 循环引用问题(copy 已处理)
a = []
a.append(a)
deepcopy 内部使用 memo 表,避免无限递归。
3️⃣ 自定义对象的拷贝行为
class A:
def __init__(self):
self.lst = []
- 默认浅拷贝。
📌 可以通过:
__copy____deepcopy__
来自定义行为(高级用法)
python
class Person:
def __init__(self, name, friends=None):
self.name = name
self.friends = friends if friends is not None else []
# 创建对象
p1 = Person("Alice")
p1.friends.append("Bob")
# 浅拷贝(默认的各种拷贝方式)
p2 = p1 # ❌ 引用,不是拷贝
p3 = Person(p1.name, p1.friends) # ❌ 浅拷贝(列表是共享的)
import copy
p4 = copy.copy(p1) # ⚠️ 浅拷贝
python
p1 = Person("Alice")
p1.friends.append("Bob")
# 浅拷贝创建 p2
p2 = copy.copy(p1)
p2.name = "Alice Clone" # ✅ 修改基本属性不影响原对象
print(p1.friends) # ['Bob']
p2.friends.append("Charlie") # ⚠️ 修改可变属性
print(p1.friends) # ['Bob', 'Charlie'] ❌ 原对象也被修改了!
自定义类深拷贝实现
copy.deepcopy:
python
import copy
class Person:
def __init__(self, name, friends=None):
self.name = name
self.friends = friends if friends is not None else []
# 深拷贝
p1 = Person("Alice", ["Bob"])
p2 = copy.deepcopy(p1) # ✅ 完全独立的拷贝
p2.friends.append("Charlie")
print(p1.friends) # ['Bob'] ✅ 不受影响
print(p2.friends) # ['Bob', 'Charlie']
自定义 __deepcopy__ 方法:
python
import copy
class Person:
def __init__(self, name, friends=None, metadata=None):
self.name = name
self.friends = friends if friends is not None else []
self.metadata = metadata if metadata is not None else {}
def __deepcopy__(self, memo):
"""自定义深拷贝行为"""
# memo 字典用于避免循环引用导致的无限递归
cls = self.__class__
result = cls.__new__(cls)
memo[id(self)] = result
# 深拷贝所有属性
result.name = copy.deepcopy(self.name, memo)
result.friends = copy.deepcopy(self.friends, memo)
result.metadata = copy.deepcopy(self.metadata, memo)
return result
# 使用
p1 = Person("Alice", ["Bob"], {"id": 1})
p2 = copy.deepcopy(p1) # 使用自定义的深拷贝
自定义 __copy__ 和 __deepcopy__:
python
import copy
class Config:
def __init__(self, settings=None):
self.settings = settings if settings else {}
self._cache = {} # 不希望被拷贝的缓存
def __copy__(self):
"""自定义浅拷贝"""
cls = self.__class__
new_obj = cls.__new__(cls)
new_obj.settings = self.settings.copy() # 只拷贝字典
new_obj._cache = {} # 新建空缓存
return new_obj
def __deepcopy__(self, memo):
"""自定义深拷贝"""
cls = self.__class__
new_obj = cls.__new__(cls)
memo[id(self)] = new_obj
# 深拷贝 settings
new_obj.settings = copy.deepcopy(self.settings, memo)
# 不拷贝缓存,新建空缓存
new_obj._cache = {}
return new_obj
# 使用
config1 = Config({"theme": "dark"})
config2 = copy.copy(config1) # 使用自定义浅拷贝
config3 = copy.deepcopy(config1) # 使用自定义深拷贝
参数传递的本质
The essence of parameter passing.
Python 参数传递的本质:传递"对象引用的拷贝"。不是值传递,不是引用传递,这是 Python 自己的一套模型。
C/C++的世界:
- 传递值:拷贝值。
- 引用传递:传地址。
Python 中:
- 没有变量地址的语法。
- 变量本身不是对象。
- 变量只是名字。
函数调用时:
python
def f(x):
...
f(a)
真实过程:
-
计算实参表达式
a得到对象10 -
创建形参名字
x -
让
x指向同一个对象。pythona ─┐ ├──▶ 10 x ─┘ -
函数参数 = 新名字 + 同一对象
python
def f(x):
x += 1
a = 10
f(a)
print(a) # 10
真实发生的事:
python
x += 1 # 等价于 x = x + 1
- 创建新对象
11 - 重新绑定
x a不受影响
📌 不是没传进去,而是"改了名字绑定"
def f(lst):
lst.append(3)
a = [1, 2]
f(a)
print(a) # [1, 2, 3]
真实发生的事:
lst和a指向同一个 list.append()修改对象本身- 所有名字都"看到变化"
📌 不是引用传递,是"对象被改了"
python
def f(lst):
lst = [100]
a = [1, 2]
f(a)
print(a) # ?
答案:
[1, 2]
📌 因为:
lst = [100]是重新绑定- 没有修改原 list 对象
1️⃣ 明确文档(推荐)
python
def add_item(lst):
"""会原地修改 lst"""
lst.append(1)
2️⃣ 内部拷贝(防副作用)
python
def add_item(lst):
lst = lst.copy()
lst.append(1)
return lst
3️⃣ 使用不可变对象(函数式思维)
python
def add_item(t):
return t + (1,)
为什么 dict / set 性能好速度快?
dict / set 使用的是"哈希表(hash table)",把"查找问题"从 O(n) 直接降到 O(1)。
这里的 hash table 指的是数据结构 hash 。
在 Python 中:
hash(obj)
👉 返回的是一个整数(int)
如果
a == b,则hash(a) == hash(b)但
hash(a) == hash(b),不一定a == b
也就是说:
- 允许 hash 冲突
- 而且必然存在冲突
在 Python 中,hash 函数会把一个对象映射为一个整数值,该值用于在哈希表中快速计算存储位置;通过 hash 定位后,再用 == 解决冲突,从而实现平均 O(1) 的查找效率。
tex
对象
↓
整数 hash(可能冲突)
↓
映射为桶索引
↓
hash 相同再用 ==