引言:为什么必须懂这些?
你是否遇到过以下'灵异现象'?
- 定义
a = [1,2,3],b = a后修改b.append(4),结果a也变成了[1,2,3,4]? - 字符串
x = "abc"和y = "abc"的id(x) == id(y)为 True,列表m = [1,2]和n = [1,2]的id(m) == id(n)却为 False?
本文系统讲解 Python 内存模型核心概念,包括内存地址获取、堆栈存储机制及小整数池优化。深入分析可变与不可变类型的区别,阐述引用赋值与函数传参原理。通过代码示例演示浅拷贝与深拷贝的差异、性能对比及实际应用场景,提供工程化避坑指南,帮助开发者掌握对象生命周期管理与内存安全。
你是否遇到过以下'灵异现象'?
a = [1,2,3],b = a 后修改 b.append(4),结果 a 也变成了 [1,2,3,4]?x = "abc" 和 y = "abc" 的 id(x) == id(y) 为 True,列表 m = [1,2] 和 n = [1,2] 的 id(m) == id(n) 却为 False?tuple 明明是'不可变类型',但修改 tuple 里的 list 元素,tuple 的内容居然变了?这些问题的根源,都在于你不了解 Python 的内存地址、堆栈模型、可变/不可变类型、引用机制、深浅拷贝——这些是 Python 的底层核心。不懂这些,你的代码永远是'靠运气运行'。
本文将从最基础的内存地址讲起,逐步深入到深浅拷贝的底层逻辑,每个概念都用可复现的代码示例验证。
内存是计算机存储数据的'仓库',每个存储单元都有一个唯一的'门牌号',这就是内存地址。在 Python 中:
id(obj) 获取对象的内存地址(10 进制整数);is 运算符判断两个对象是否是同一个(即是否在同一内存地址)。代码验证:
x = 10
print(f"x 的值:{x}")
print(f"x 的内存地址(id):{id(x)}")
# 输出示例:140703383430720
y = x
print(f"y 的值:{y}")
print(f"y 的内存地址:{id(y)}")
# 与 x 的 id 完全相同:140703383430720
print(f"x 和 y 是否是同一个对象?{x is y}")
# 输出:True
结论:y = x 是将 x 的内存地址拷贝给 y,而非拷贝值,因此 x 和 y 指向同一个对象。
在 C/C++ 中,堆和栈的区别是:
但Python 的内存模型是'栈存引用,堆存对象':
代码验证:
def func():
a = [1,2,3]
# 栈上存储 a 指向的内存地址,堆上存储 [1,2,3] 对象
print(f"函数内 a 的地址:{id(a)}")
# 示例:140703383221696
func()
# 函数执行完毕后,栈上的 a 被销毁,但堆上的 [1,2,3] 对象若没有其他引用,会被 GC 回收
a = [1,2,3]
# 堆上再次创建 [1,2,3] 对象,但地址与函数内不同
print(f"函数外 a 的地址:{id(a)}")
# 示例:140703383221760
为了节省内存和提高性能,Python 会对部分对象进行预创建或缓存:
Python 启动时会预创建 -5 到 256 的整数对象,这些对象会被所有代码共享,永远不会被 GC 回收。
代码验证:
x = 256
y = 256
print(f"x 的地址:{id(x)}")
# 示例:140703383430720
print(f"y 的地址:{id(y)}")
# 与 x 相同:140703383430720
print(f"x is y:{x is y}")
# True
x = 257
y = 257
print(f"x 的地址:{id(x)}")
# 示例:140703383430752
print(f"y 的地址:{id(y)}")
# 示例:140703383430784(不同地址)
print(f"x is y:{x is y}")
# False(257 不在小整数池内)
Python 会对满足条件的字符串进行'驻留'(缓存),避免重复创建。条件是:
代码验证:
x = "abc123"
y = "abc123"
print(f"x is y:{x is y}")
# True(满足驻留条件)
x = "abc 123"
# 包含空格,不满足驻留条件
y = "abc 123"
print(f"x is y:{x is y}")
# False
注意:不要用 is 比较字符串的相等性,永远用 ==——字符串驻留的规则复杂且依赖 Python 版本,无法保证。
分类表:
| 类型 | 可变 / 不可变 | 示例 |
|---|---|---|
| int | 不可变 | 10 |
| float | 不可变 | 3.14 |
| str | 不可变 | "hello" |
| tuple | 不可变 | (1,2) |
| bool | 不可变 | True |
| None | 不可变 | None |
| list | 可变 | [1,2,3] |
| dict | 可变 | {"name": "张三"} |
| set | 可变 | {1,2,3} |
代码验证(int 类型):
x = 10
print(f"x=10 的地址:{id(x)}")
# 示例:140703383430720
x += 5
# 修改不可变类型,创建新对象
print(f"x=15 的地址:{id(x)}")
# 示例:140703383430880(新地址)
print(f"x 的当前值:{x}")
# 15
代码验证(str 类型):
s = "hello"
print(f"s='hello'的地址:{id(s)}")
# 示例:140703383221696
s += " world"
# 创建新字符串对象
print(f"s='hello world'的地址:{id(s)}")
# 示例:140703383221760(新地址)
代码验证(tuple 类型):
t = (1,2)
print(f"t=(1,2)的地址:{id(t)}")
# 示例:140703383221824
# t[0] = 3 → 报错:TypeError: 'tuple' object does not support item assignment(不可修改)
代码验证(list 类型):
a = [1,2,3]
print(f"a=[1,2,3]的地址:{id(a)}")
# 示例:140703383221888
a.append(4)
# 修改可变类型,更新原对象
print(f"a=[1,2,3,4]的地址:{id(a)}")
# 仍然是 140703383221888
a[0] = 10
# 修改元素,地址不变
print(f"a=[10,2,3,4]的地址:{id(a)}")
# 140703383221888
代码验证(dict 类型):
d = {"name": "张三", "age": 20}
print(f"d 的地址:{id(d)}")
# 示例:140703383221952
d["age"] = 21
# 修改值,地址不变
print(f"d 的地址:{id(d)}")
# 140703383221952
d["city"] = "北京"
# 添加键值对,地址不变
print(f"d 的地址:{id(d)}")
# 140703383221952
tuple 是不可变类型,但如果 tuple 的元素是可变类型(比如 list),则可以修改这个可变元素的内容——这是因为 tuple 的'不可变'是指元素的引用不可变,而非元素本身的内容不可变。
代码验证:
t = ([1,2], 3)
# tuple 包含一个 list
print(f"t 的地址:{id(t)}")
# 示例:140703383222080
print(f"t 的内容:{t}")
# ([1,2], 3)
# 修改 tuple 里的 list 内容
t[0].append(3)
print(f"t 的地址:{id(t)}")
# 仍然是 140703383222080
print(f"t 的内容:{t}")
# ([1,2,3], 3)(内容变了!)
# 尝试修改 tuple 的元素引用 → 报错
# t[0] = [4,5] → TypeError: 'tuple' object does not support item assignment
避坑指南:永远不要在 tuple 里放可变类型,否则会导致'不可变'的语义失效。
在 Python 中,'引用'是指向堆上对象的内存地址(类比 C/C++ 的指针,但 Python 不允许直接操作指针)。所有赋值操作都是引用赋值——没有'值赋值'的概念。
代码验证:
a = [1,2,3]
b = a
# 拷贝 a 的引用,b 与 a 指向同一个对象
print(f"a 的地址:{id(a)}")
# 示例:140703383222144
print(f"b 的地址:{id(b)}")
# 140703383222144
print(f"a is b:{a is b}")
# True
# 修改 b,a 也会变
b.append(4)
print(f"a 的内容:{a}")
# [1,2,3,4]
print(f"b 的内容:{b}")
# [1,2,3,4]
Python 的函数传参是引用传递——不是值传递,也不是指针传递。也就是说,函数内部的参数是外部对象的引用,修改这个参数会影响外部对象(如果是可变类型)。
代码验证(可变类型):
def modify_list(lst):
lst.append(4)
# 修改原对象
a = [1,2,3]
modify_list(a)
print(f"外部 a 的内容:{a}")
# [1,2,3,4](被修改了!)
代码验证(不可变类型):
def modify_int(x):
x += 5
# 创建新对象,不会影响外部
b = 10
modify_int(b)
print(f"外部 b 的内容:{b}")
# 10(未被修改)
避坑指南:若不想让函数修改外部可变对象,需在函数内部拷贝参数。
==:值相等——比较两个对象的内容是否相同;is:身份相等——比较两个对象的内存地址是否相同。代码验证:
a = [1,2,3]
b = [1,2,3]
print(f"a == b:{a == b}")
# True(内容相同)
print(f"a is b:{a is b}")
# False(不同内存地址)
x = None
y = None
print(f"x is y:{x is y}")
# True(None 是单例对象,只有一个内存地址)
最佳实践:
== 比较字符串、列表、字典等的内容;is 比较 None、True、False 等单例对象。因为 Python 的赋值是引用赋值,当我们需要修改一个对象,但不想影响原对象时,就需要拷贝。常见场景:
浅拷贝仅拷贝对象的第一层结构,对于嵌套对象(如 [[1,2], [3,4]]),只会拷贝它们的引用,不会拷贝实际内容。
代码验证:
import copy
# 嵌套列表
a = [[1,2], [3,4]]
b = copy.copy(a)
# 浅拷贝
print(f"a 的地址:{id(a)}")
# 示例:140703383222208
print(f"b 的地址:{id(b)}")
# 示例:140703383222272(新地址,浅拷贝成功)
print(f"a is b:{a is b}")
# False
# 嵌套对象的引用仍然相同
print(f"a[0] 的地址:{id(a[0])}")
# 示例:140703383222336
print(f"b[0] 的地址:{id(b[0])}")
# 140703383222336(同一个地址)
print(f"a[0] is b[0]:{a[0] is b[0]}")
# True
# 修改 b 的嵌套对象,a 也会变
b[0][0] = 5
print(f"a 的内容:{a}")
# [[5,2], [3,4]](a 被修改了!)
print(f"b 的内容:{b}")
# [[5,2], [3,4]]
深拷贝会递归拷贝对象的所有层级,包括嵌套的对象,拷贝后的对象与原对象完全独立,修改任何层级的内容都不会影响原对象。
代码验证:
import copy
a = [[1,2], [3,4]]
c = copy.deepcopy(a)
# 深拷贝
print(f"a 的地址:{id(a)}")
# 示例:140703383222208
print(f"c 的地址:{id(c)}")
# 示例:140703383222400(新地址)
print(f"a is c:{a is c}")
# False
# 嵌套对象也被拷贝了
print(f"a[0] 的地址:{id(a[0])}")
# 示例:140703383222336
print(f"c[0] 的地址:{id(c[0])}")
# 示例:140703383222464(新地址)
print(f"a[0] is c[0]:{a[0] is c[0]}")
# False
# 修改 c 的嵌套对象,a 不变
c[0][0] = 1
print(f"a 的内容:{a}")
# [[5,2], [3,4]](a 未被修改)
print(f"c 的内容:{c}")
# [[1,2], [3,4]]
除了 copy.copy(),Python 还有以下内置的浅拷贝操作:
copy() 方法:a.copy();copy() 方法:d.copy();a[:];list(a)、dict(d)、set(s)。代码验证:
a = [[1,2], [3,4]]
b = a.copy() # 浅拷贝
c = a[:] # 浅拷贝
d = list(a) # 浅拷贝
print(f"b[0] is a[0]:{b[0] is a[0]}")
# True
print(f"c[0] is a[0]:{c[0] is a[0]}")
# True
print(f"d[0] is a[0]:{d[0] is a[0]}")
# True
深拷贝需要递归拷贝所有层级的对象,因此比浅拷贝慢很多。在不需要深拷贝的场景下,应尽量使用浅拷贝或赋值。
代码验证:
import copy
import time
# 创建一个复杂的嵌套列表(1000×1000)
a = [[i for i in range(1000)] for _ in range(1000)]
# 浅拷贝时间
start = time.time()
b = copy.copy(a)
end = time.time()
print(f"浅拷贝时间:{end - start:.4f}秒")
# 约 0.0001 秒
# 深拷贝时间
start = time.time()
c = copy.deepcopy(a)
end = time.time()
print(f"深拷贝时间:{end - start:.4f}秒")
# 约 0.1 秒(慢 1000 倍!)
| 参数类型 | 拷贝策略 | 理由 |
|---|---|---|
| 不可变类型 | 直接传递引用 | 修改不会影响外部 |
| 可变类型(简单结构) | 浅拷贝 | 仅需第一层独立,性能高 |
| 可变类型(嵌套结构) | 深拷贝 | 需要完全独立,避免内层修改影响外层 |
代码验证(嵌套结构传参):
import copy
def process_config(config):
# 深拷贝,避免修改外部配置
config_copy = copy.deepcopy(config)
config_copy["db"]["port"] = 3307
# 修改数据库端口
return config_copy
# 原配置
config = {"db": {"host": "localhost", "port": 3306}}
service_config = process_config(config)
print(f"原配置端口:{config['db']['port']}")
# 3306(未被修改)
print(f"服务配置端口:{service_config['db']['port']}")
# 3307
坑点:函数默认参数在函数定义时就创建,而非在调用时创建。如果默认参数是可变类型,会导致所有调用共享同一个对象。
错误示例:
def func(lst=[]):
lst.append(1)
return lst
print(func()) # [1]
print(func()) # [1,2](共享同一个列表!)
print(func()) # [1,2,3]
正确示例:
def func(lst=None):
if lst is None:
lst = [] # 每次调用都创建新列表
lst.append(1)
return lst
print(func()) # [1]
print(func()) # [1]
print(func()) # [1]
代码验证(配置文件场景):
# 全局配置
global_config = {
"server": {"port": 8080},
"log": {"level": "INFO"}
}
# 服务 1 配置:完全独立,修改不影响全局
import copy
service1_config = copy.deepcopy(global_config)
service1_config["server"]["port"] = 8081
# 服务 2 配置:仅第一层独立,日志配置与全局共享
service2_config = copy.copy(global_config)
service2_config["server"]["port"] = 8082
print(f"全局端口:{global_config['server']['port']}")
# 8080
print(f"服务 1 端口:{service1_config['server']['port']}")
# 8081
print(f"服务 2 端口:{service2_config['server']['port']}")
# 8082
| 坑点 | 解决方案 |
|---|---|
| 修改 A 导致 B 变 | 用浅拷贝或深拷贝创建独立副本 |
| tuple 的内容'变了' | 避免在 tuple 中放可变类型 |
| 函数默认参数共享 | 用 None 作为默认参数,在函数内部创建新对象 |
| 字符串 id () 相同/不同 | 用 == 比较内容,不用 is |
| 深拷贝性能差 | 仅在必要时使用深拷贝,或优化数据结构 |
Python 的内存模型是其所有核心特性的基础,理解这些概念,可以帮助你:
核心口诀:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online