大家好,我是正在实战各种 AI 项目的程序员晚枫。

你有没有遇到过这种情况:明明只改了一个变量,另一个变量也跟着变了? 这种 bug 特别隐蔽,我当年做项目的时候踩过无数次坑。今天咱们就把 Python 的引用机制彻底搞明白——搞懂了,这种 bug 再也难不住你。


🔗 引用与身份:变量不是盒子,是标签

一个场景引入

1
2
3
4
5
6
7
8
9
# 你以为 a 和 b 是两个独立的列表?
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] 😱 a 也被改了!

# 这不是 bug,这是引用的真相
print(a is b) # True —— a 和 b 指向同一个对象
print(id(a) == id(b)) # True —— 内存地址相同

💡 核心概念:Python 中的变量不是"盒子"(存放值),而是"标签"(贴在对象上)。a = [1,2,3] 不是把列表放进 a 这个盒子,而是把 a 这个标签贴到了 [1,2,3] 这个对象上。

共享引用的几种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 情况1:简单赋值 → 共享引用
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表
b.append(4)
print(a) # [1, 2, 3, 4] —— 被修改了

# 情况2:函数传参 → 也是共享引用
def add_item(lst):
lst.append("new")
return lst

data = [1, 2, 3]
result = add_item(data)
print(data) # [1, 2, 3, 'new'] —— 原列表被改了!
print(result) # [1, 2, 3, 'new']
print(data is result) # True —— 同一个对象

# 情况3:不可变对象 → 看起来"安全"但不完全安全
s1 = "hello"
s2 = s1
s2 = "world" # s2 指向了新对象,s1 不受影响
print(s1) # hello
print(s2) # world

值相等 vs 身份相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b) # True —— 值相等(__eq__ 比较)
print(a is b) # False —— 身份不同(不是同一个对象)

# 小整数和字符串的特殊情况(驻留机制)
x = 256
y = 256
print(x is y) # True —— 小整数被缓存

x = 257
y = 257
print(x is y) # False —— 超出缓存范围(在交互模式中)

s1 = "hello"
s2 = "hello"
print(s1 is s2) # True —— 字符串驻留

⚠️ 面试必知== 比较值,is 比较身份(内存地址)。判断 Noneis None,不用 == None


📋 深浅拷贝:复制也有大学问

浅拷贝:只复制第一层

1
2
3
4
5
6
7
8
9
10
11
12
import copy

original = [[1, 2], [3, 4]]
shallow = copy.copy(original)

# 修改内层对象 → 原对象受影响!
shallow[0][0] = 'X'
print(original) # [['X', 2], [3, 4]] —— 内层被修改

# 修改外层 → 原对象不受影响
shallow.append([5, 6])
print(original) # [['X', 2], [3, 4]] —— 外层独立

深拷贝:递归复制所有层

1
2
3
4
5
6
7
8
import copy

original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)

deep[0][0] = 'Y'
print(original) # [[1, 2], [3, 4]] —— 完全不受影响
print(deep) # [['Y', 2], [3, 4]]

浅拷贝的多种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import copy

data = [1, 2, 3, 4, 5]

# 方式1:copy.copy()
c1 = copy.copy(data)

# 方式2:切片
c2 = data[:]

# 方式3:构造函数
c3 = list(data)

# 方式4:列表推导
c4 = [x for x in data]

# 方式5:copy 方法(字典和集合)
d = {'a': 1, 'b': 2}
c5 = d.copy()

# 性能对比
import timeit
print(timeit.timeit('copy.copy(data)', globals=globals(), number=100000)) # 较慢
print(timeit.timeit('data[:]', globals=globals(), number=100000)) # 最快
print(timeit.timeit('list(data)', globals=globals(), number=100000)) # 较快

何时使用哪种拷贝

场景推荐原因性能
简单扁平列表切片 [:]最快⭐⭐⭐⭐⭐
简单扁平列表list()⭐⭐⭐⭐
嵌套对象copy.deepcopy()安全⭐⭐
只读访问不拷贝节省内存⭐⭐⭐⭐⭐
字典浅拷贝.copy()惯用写法⭐⭐⭐⭐

🔄 可变与不可变:一张表搞清楚

核心区别

类型可变性示例修改时行为
list✅ 可变[1,2,3]原地修改
dict✅ 可变{'a':1}原地修改
set✅ 可变{1,2,3}原地修改
tuple❌ 不可变(1,2,3)创建新对象
str❌ 不可变"hello"创建新对象
int/float❌ 不可变42创建新对象
frozenset❌ 不可变frozenset({1})创建新对象

可变对象的陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
# 陷阱1:字典的键必须是不可变类型
# my_dict = {[1,2]: 'value'} # TypeError! 列表不可哈希
my_dict = {(1,2): 'value'} # 元组可以

# 陷阱2:可变对象作为集合元素
# my_set = {[1,2], [3,4]} # TypeError!
my_set = {(1,2), (3,4)} # 元组可以

# 陷阱3:元组内的可变对象
t = ([1, 2], [3, 4])
t[0].append(3) # 可以修改内部列表!
print(t) # ([1, 2, 3], [3, 4])
# t[0] = 'new' # 但不能替换元组元素

⚠️ 可变默认参数:Python 最大坑之一

经典 Bug

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 错误示例:可变默认参数
class BugExample:
def __init__(self, items=[]): # 危险!
self.items = items

a = BugExample()
a.items.append(1)
b = BugExample()
print(b.items) # [1] 😱 b 的默认值被 a 修改了!

# 原因:默认参数在函数定义时创建,所有调用共享同一个对象
print(a.items is b.items) # True

正确写法

1
2
3
4
5
6
7
8
9
# ✅ 正确做法:用 None 作为默认值
class GoodExample:
def __init__(self, items=None):
self.items = items if items is not None else []

a = GoodExample()
a.items.append(1)
b = GoodExample()
print(b.items) # [] —— 安全!

函数中的同类陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 函数默认参数也是同样的坑
def append_to(element, target=[]):
target.append(element)
return target

print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] —— 上次的结果还在!

# ✅ 正确写法
def append_to_fixed(element, target=None):
if target is None:
target = []
target.append(element)
return target

print(append_to_fixed(1)) # [1]
print(append_to_fixed(2)) # [2] —— 安全!

🏗️ 实战案例:数据缓存系统的引用问题

场景:配置管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import copy

class ConfigManager:
"""配置管理器 —— 正确处理引用和拷贝"""

def __init__(self, default_config):
# 深拷贝默认配置,避免外部修改影响内部
self._default = copy.deepcopy(default_config)
self._configs = {}

def get_config(self, name):
"""获取配置,返回深拷贝防止外部修改"""
if name not in self._configs:
# 返回默认配置的深拷贝
return copy.deepcopy(self._default)
return copy.deepcopy(self._configs[name])

def update_config(self, name, updates):
"""更新配置,基于当前配置的拷贝修改"""
config = self.get_config(name)
config.update(updates)
self._configs[name] = config
return config

def reset_config(self, name):
"""重置为默认配置"""
if name in self._configs:
del self._configs[name]

# 使用
default = {'debug': False, 'port': 8080, 'hosts': ['localhost']}
mgr = ConfigManager(default)

# 外部修改不会影响管理器
external = mgr.get_config('app')
external['hosts'].append('example.com')
print(mgr.get_config('app')['hosts']) # ['localhost'] —— 安全!

性能对比:拷贝方式的选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import copy
import timeit

# 测试数据
simple_data = list(range(1000))
nested_data = {'users': [{'name': f'user_{i}', 'age': i} for i in range(100)]}

# 简单数据
print("简单列表拷贝性能(10万次):")
print(f" 切片: {timeit.timeit('simple_data[:]', globals=globals(), number=100000):.3f}s")
print(f" list(): {timeit.timeit('list(simple_data)', globals=globals(), number=100000):.3f}s")
print(f" copy.copy: {timeit.timeit('copy.copy(simple_data)', globals=globals(), number=100000):.3f}s")
print(f" deepcopy: {timeit.timeit('copy.deepcopy(simple_data)', globals=globals(), number=100000):.3f}s")

# 嵌套数据
print("嵌套字典拷贝性能(1万次):")
print(f" copy.copy: {timeit.timeit('copy.copy(nested_data)', globals=globals(), number=10000):.3f}s")
print(f" deepcopy: {timeit.timeit('copy.deepcopy(nested_data)', globals=globals(), number=10000):.3f}s")

🎯 本讲总结

引用与身份is 比较身份(同一对象),== 比较值。变量是标签不是盒子。

深浅拷贝copy() 只复制第一层,deepcopy() 递归复制所有层。简单对象用切片,嵌套对象用深拷贝。

可变默认参数:永远不要用可变对象(list/dict/set)作为函数默认参数,用 None 代替。

可变 vs 不可变:理解哪些类型是可变的,避免在字典键、集合元素、默认参数中使用可变对象。


📚 推荐教材

《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》

学习路线: 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》


🎓 加入《流畅的 Python》直播共读营

学到这里,如果你想系统吃透这本书——欢迎加入我的直播共读课。

  • 每周直播精讲,逐章拆解核心知识点
  • 专属学习群,随时答疑交流
  • 试运营特惠:499 元299 元

👉 【立即报名《流畅的 Python》共读课】https://mp.weixin.qq.com/s/ivHJwn1nNx5ug4TFrapvGg

🔗 课程导航

上一讲:可调用对象 | 下一讲:序列和多态


💬 联系我

平台账号/链接
微信扫码加好友
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!

fluent-python.png