
面向读者:所有 Python 开发者(从入门到进阶)
核心价值:用50 + 代码示例、20 + 内存地址验证,彻底拆解 Python 内存地址、堆栈模型、可变 / 不可变类型、引用机制、深浅拷贝的底层逻辑,解决 "修改 A 导致 B 变" 等 90% 以上的 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)}") # 140703383221952tuple 是不可变类型,但如果 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 的内存模型是其所有核心特性的基础,理解这些概念,可以帮助你:
核心口诀: