关注

Python快速入门专业版(十四):变量赋值的“陷阱”:浅拷贝与深拷贝(用代码看懂内存地址)

在这里插入图片描述

目录

引言:为什么改了b,a也跟着变?

1.赋值的本质:不是值传递,而是引用传递

1.1 用id()函数看穿内存地址

场景1:不可变对象的赋值(无副作用)

场景2:可变对象的赋值(有副作用)

1.2 不可变对象的“特殊情况”:小整数池与字符串驻留

2.浅拷贝(Shallow Copy):只复制“外层壳子”

2.1 浅拷贝的4种实现方式

代码示例:列表的浅拷贝

2.2 浅拷贝的“隐形陷阱”:内层对象仍共享

代码演示:浅拷贝的内层共享问题

2.3 浅拷贝的适用场景

3.深拷贝(Deep Copy):复制“所有层级”的完全独立

3.1 深拷贝的实现:copy.deepcopy()

代码示例:深拷贝的完全独立性

3.2 深拷贝的性能代价:递归复制的开销

代码示例:浅拷贝vs深拷贝的性能对比

4.浅拷贝vs深拷贝:3分钟看懂核心区别

4.1 对比实验:修改不同层级的数据

4.2 核心区别总结表

5.实战避坑:5个高频场景的正确拷贝方式

场景1:函数参数避免修改外部数据

场景2:配置文件的个性化修改

场景3:列表去重(单层对象,浅拷贝足够)

场景4:性能敏感场景的“手动部分拷贝”

场景5:避免“默认参数陷阱”

总结:3步选择正确的拷贝方式


引言:为什么改了b,a也跟着变?

你是否遇到过这样的困惑:明明只修改了列表b,却发现列表a的值也跟着变了?在Python中,这不是bug,而是变量赋值的“底层逻辑”导致的——Python的变量本质是“对象的引用”(类似标签),赋值操作a = b不是复制数据,而是给同一块内存里的对象贴了两个标签

这种“引用传递”的特性,在处理整数、字符串等不可变对象时影响不大,但在处理列表、字典等可变对象时,很容易引发“牵一发而动全身”的隐性bug。本文将通过id()函数可视化内存地址,从“赋值本质→浅拷贝局限→深拷贝解决方案”层层拆解,结合实战案例帮你避开拷贝陷阱,精准控制数据独立性。所有代码基于Python 3.13.6测试,可直接复现。

1.赋值的本质:不是值传递,而是引用传递

在Python中,“变量”和“数据”是分离的——数据(如列表、整数)存放在内存中,变量只是指向这片内存的“引用”(类似地址标签)。赋值操作a = b的核心是“让ab指向同一片内存”,而非“把b的数据复制给a”。

1.1 用id()函数看穿内存地址

id(object)是Python的内置函数,返回对象的唯一内存地址标识符(整数)。通过比较两个变量的id,就能判断它们是否指向同一个对象。

场景1:不可变对象的赋值(无副作用)

不可变对象(整数、字符串、元组等)的核心特点是“数据创建后无法修改”——若要“修改”,本质是创建新对象并让变量指向新内存。因此,不可变对象的赋值不会出现“改一个影响另一个”的问题。

# 示例1:整数(不可变)
x = 10
y = x  # y和x指向同一块内存(存储10的地址)
print(f"赋值后:x的地址={id(x)}, y的地址={id(y)}")  # 输出相同地址,如2898567296528

# “修改”y:实际是创建新对象(存储20),y指向新地址
y = 20
print(f"修改后:x的地址={id(x)}, y的地址={id(y)}")  # x地址不变,y地址变化
print(f"x的值={x}, y的值={y}")  # 输出:x=10, y=20(x不受影响)

# 示例2:字符串(不可变)
s1 = "hello"
s2 = s1  # s2和s1指向同一字符串
print(f"赋值后:s1地址={id(s1)}, s2地址={id(s2)}")  # 地址相同

# “修改”s2:创建新字符串"hello world",s2指向新地址
s2 += " world"
print(f"修改后:s1={s1}, s2={s2}")  # 输出:s1=hello, s2=hello world(s1不受影响)

关键原理:不可变对象的“修改”本质是“创建新对象”,原变量仍指向旧对象,因此不会相互影响。

场景2:可变对象的赋值(有副作用)

可变对象(列表、字典、集合等)的核心特点是“数据可直接修改”——修改操作会直接改变内存中的数据,而非创建新对象。因此,若两个变量指向同一个可变对象,修改其中一个会同步影响另一个。

# 示例1:列表(可变)
a = [1, 2, 3]
b = a  # a和b指向同一块内存(存储列表[1,2,3]的地址)
print(f"赋值后:a地址={id(a)}, b地址={id(b)}")  # 地址相同,如2451458888256

# 修改b的元素:直接修改内存中的列表数据
b[0] = 100  # 改变列表第一个元素的值
print(f"修改后:a={a}, b={b}")  # 输出:a=[100,2,3], b=[100,2,3](a同步变化)
print(f"修改后:a地址={id(a)}, b地址={id(b)}")  # 地址仍相同(未创建新对象)

# 示例2:字典(可变)
dict1 = {"name": "Alice", "age": 25}
dict2 = dict1  # 指向同一字典
dict2["age"] = 26  # 修改dict2的age字段
print(f"dict1={dict1}, dict2={dict2}")  # 输出:dict1={"name":"Alice","age":26}, dict2=...(同步变化)

致命陷阱:新手常误以为b = a是“复制列表”,实际只是“复制引用”——ab是同一列表的“两个名字”,改一个必然影响另一个。

1.2 不可变对象的“特殊情况”:小整数池与字符串驻留

Python为优化性能,对部分不可变对象做了“缓存复用”,导致看似“不同对象”却指向同一内存,这是赋值逻辑的“例外情况”,但不影响核心原理。

  • 小整数池:对-5~256范围内的整数,Python会提前创建并缓存,所有赋值都指向同一对象;
  • 字符串驻留:对纯字母、数字、下划线组成的短字符串,Python会缓存并复用。
# 小整数池示例:256以内的整数复用内存
x = 100
y = 100
print(id(x) == id(y))  # 输出:True(指向同一对象)

x = 300  # 超出小整数池范围
y = 300
print(id(x) == id(y))  # 输出:False(创建两个不同对象)

# 字符串驻留示例:纯字母数字字符串复用
s1 = "python123"
s2 = "python123"
print(id(s1) == id(s2))  # 输出:True(复用缓存)

s1 = "python 123"  # 含空格,不满足驻留条件
s2 = "python 123"
print(id(s1) == id(s2))  # 输出:False(创建新对象)

注意:这是Python的优化细节,不改变“不可变对象赋值无副作用”的核心结论——即使xy指向同一对象,“修改”时仍会创建新对象。

2.浅拷贝(Shallow Copy):只复制“外层壳子”

为解决“可变对象赋值同步变化”的问题,需要复制对象本身而非引用。浅拷贝是最常用的拷贝方式,它会创建一个“新的外层对象”,但内层嵌套的可变对象仍共享引用——相当于“复制了壳子,没复制里面的内容”。

2.1 浅拷贝的4种实现方式

Python中针对不同对象,有多种浅拷贝方法,核心效果一致:

对象类型浅拷贝方法示例
列表1. list.copy()
2. 切片a[:]
3. list(a)
a = [1,2,3]; b = a.copy()
字典1. dict.copy()
2. dict(a)
a = {"k":1}; b = a.copy()
集合1. set.copy()
2. set(a)
a = {1,2}; b = a.copy()
通用对象copy模块的copy()函数import copy; b = copy.copy(a)

代码示例:列表的浅拷贝

import copy

# 原始列表(含嵌套列表,模拟“外层+内层”结构)
a = [1, 2, [3, 4]]  # 外层:[1,2, 内层列表];内层:[3,4]

# 方法1:list.copy()
b = a.copy()
# 方法2:切片(最简洁,推荐)
c = a[:]
# 方法3:list()构造函数
d = list(a)
# 方法4:copy模块的copy()(通用)
e = copy.copy(a)

# 验证:外层对象是新的(地址不同)
print(f"原列表a地址:{id(a)}")
print(f"拷贝后b地址:{id(b)},与a是否相同:{id(b) == id(a)}")  # 输出:False
print(f"拷贝后c地址:{id(c)},与a是否相同:{id(c) == id(a)}")  # 输出:False

2.2 浅拷贝的“隐形陷阱”:内层对象仍共享

浅拷贝仅复制“外层对象”,对于内层嵌套的可变对象(如列表中的列表、字典中的列表),新对象和原对象仍共享引用——修改内层数据,两边会同步变化,这是浅拷贝最容易被忽略的问题。

代码演示:浅拷贝的内层共享问题

import copy

# 原始列表:外层列表+内层嵌套列表(可变对象)
a = [1, 2, [3, 4]]
b = a.copy()  # 浅拷贝

# 场景1:修改外层元素(互不影响)
b[0] = 100  # 修改b的外层元素(索引0)
print(f"a的外层:{a[0]},b的外层:{b[0]}")  # 输出:a=1,b=100(外层独立)
print(f"a的完整列表:{a},b的完整列表:{b}")  # 输出:a=[1,2,[3,4]], b=[100,2,[3,4]]

# 场景2:修改内层嵌套列表(同步变化)
b[2][0] = 300  # 修改b的内层列表(索引2是内层列表,再改索引0)
print(f"\na的内层列表:{a[2]},b的内层列表:{b[2]}")  # 输出:a=[300,4], b=[300,4](同步变化)
print(f"a的完整列表:{a},b的完整列表:{b}")  # 输出:a=[1,2,[300,4]], b=[100,2,[300,4]]

# 验证内层地址:a和b的内层列表指向同一内存
print(f"\na的内层列表地址:{id(a[2])},b的内层列表地址:{id(b[2])}")  # 地址相同

原理图解

  • 浅拷贝后,ab是两个不同的外层列表(地址不同);
  • a[2]b[2]指向同一个内层列表(地址相同),因此修改内层会联动。

2.3 浅拷贝的适用场景

浅拷贝并非“没用”,以下场景下优先使用浅拷贝(性能比深拷贝高):

  1. 对象无嵌套:如单层列表[1,2,3]、单层字典{"k1":1, "k2":2}——无内层可变对象,浅拷贝后完全独立;
  2. 内层是不可变对象:如列表[1, "hello", (3,4)]——内层元组是不可变对象,即使共享引用,也无法修改,因此安全;
  3. 仅需修改外层:如仅需添加/删除外层元素,不碰内层数据。
# 适用场景1:单层列表(无嵌套)
a = [1, 2, 3]
b = a.copy()
b.append(4)  # 仅修改外层
print(f"a={a}, b={b}")  # 输出:a=[1,2,3], b=[1,2,3,4](完全独立)

# 适用场景2:内层是不可变对象(元组)
a = [1, "hi", (3,4)]
b = a.copy()
b[2] = (5,6)  # “修改”内层元组:实际创建新元组,不影响a
print(f"a={a}, b={b}")  # 输出:a=[1,"hi",(3,4)], b=[1,"hi",(5,6)](安全)

3.深拷贝(Deep Copy):复制“所有层级”的完全独立

当对象包含多层嵌套的可变对象(如[1, [2, [3,4]]]{"db": {"host": "localhost", "port": 3306}})时,浅拷贝的“内层共享”问题会导致数据混乱,此时需要深拷贝——递归复制所有层级的对象,新对象与原对象完全独立,修改任何层级都不会相互影响。

3.1 深拷贝的实现:copy.deepcopy()

深拷贝仅有一种通用实现方式:copy模块的deepcopy()函数,它会自动递归处理所有嵌套层级,无论多少层可变对象,都能完全复制。

代码示例:深拷贝的完全独立性

import copy

# 复杂嵌套对象:列表→字典→列表(多层可变对象)
a = [
    1,
    {"name": "Alice", "hobbies": ["reading", "coding"]},  # 内层字典+列表
    [5, 6, [7, 8]]  # 内层列表嵌套列表
]

# 深拷贝
b = copy.deepcopy(a)

# 验证:所有层级的地址均不同(完全独立)
print(f"外层地址:a={id(a)}, b={id(b)} → 不同")  # 外层不同
print(f"内层字典地址:a[1]={id(a[1])}, b[1]={id(b[1])} → 不同")  # 字典不同
print(f"字典内列表地址:a[1]['hobbies']={id(a[1]['hobbies'])}, b[1]['hobbies']={id(b[1]['hobbies'])} → 不同")  # 列表不同
print(f"深层列表地址:a[2][2]={id(a[2][2])}, b[2][2]={id(b[2][2])} → 不同")  # 深层列表不同

# 修改任意层级:均不影响原对象
b[0] = 100  # 修改外层
b[1]["name"] = "Bob"  # 修改内层字典
b[1]["hobbies"].append("running")  # 修改字典内的列表
b[2][2][0] = 700  # 修改深层列表

# 对比原对象和深拷贝对象
print(f"\n原对象a:{a}")
print(f"深拷贝对象b:{b}")
# 输出结果:a的所有值未变,b的修改完全独立

核心效果:深拷贝后,ab是“两个完全无关的对象”,无论嵌套多少层,修改其中一个都不会影响另一个。

3.2 深拷贝的性能代价:递归复制的开销

深拷贝的“完全独立”是有代价的——它需要递归遍历所有层级并复制,因此比浅拷贝慢,且消耗更多内存。数据越复杂、嵌套越深,性能差异越明显。

代码示例:浅拷贝vs深拷贝的性能对比

import copy
import time

# 构建复杂嵌套数据(1000个内层列表,每层含10个元素)
complex_data = []
for i in range(1000):
    complex_data.append([j for j in range(10)])  # 外层列表+1000个内层列表

# 测试浅拷贝耗时
start = time.time()
shallow_copy = copy.copy(complex_data)
shallow_time = time.time() - start

# 测试深拷贝耗时
start = time.time()
deep_copy = copy.deepcopy(complex_data)
deep_time = time.time() - start

# 输出结果(单位:秒)
print(f"浅拷贝耗时:{shallow_time:.6f}")  # 约0.0001秒
print(f"深拷贝耗时:{deep_time:.6f}")    # 约0.01秒(慢100倍)
print(f"深拷贝比浅拷贝慢约{int(deep_time/shallow_time)}倍")

性能结论

  • 简单数据:浅拷贝和深拷贝性能差异可忽略;
  • 复杂嵌套数据:深拷贝耗时是浅拷贝的10~100倍,需谨慎使用。

4.浅拷贝vs深拷贝:3分钟看懂核心区别

为了更直观区分,我们用“多层嵌套字典”作为测试对象,对比赋值、浅拷贝、深拷贝的效果差异:

4.1 对比实验:修改不同层级的数据

import copy

# 原始数据:多层嵌套字典(模拟配置文件场景)
original = {
    "app": "PythonCopyDemo",
    "settings": {
        "log": {
            "level": "INFO",
            "path": "./logs"
        },
        "timeout": [30, 60]  # 内层可变列表
    }
}

# 1. 赋值(引用传递)
assign_copy = original

# 2. 浅拷贝
shallow_copy = copy.copy(original)

# 3. 深拷贝
deep_copy = copy.deepcopy(original)

# 修改原始数据的3个层级
original["app"] = "ModifiedApp"  # 层级1:外层字符串(不可变)
original["settings"]["log"]["level"] = "DEBUG"  # 层级3:深层字典(可变)
original["settings"]["timeout"][0] = 10  # 层级2:内层列表(可变)

# 对比结果
print("=== 1. 赋值(引用传递)===")
print(f"assign_copy['app']: {assign_copy['app']} → 同步修改(同对象)")
print(f"assign_copy['settings']['log']['level']: {assign_copy['settings']['log']['level']} → 同步修改")
print(f"assign_copy['settings']['timeout'][0]: {assign_copy['settings']['timeout'][0]} → 同步修改")

print("\n=== 2. 浅拷贝 ===")
print(f"shallow_copy['app']: {shallow_copy['app']} → 未修改(外层字符串不可变,创建新对象)")
print(f"shallow_copy['settings']['log']['level']: {shallow_copy['settings']['log']['level']} → 同步修改(内层共享)")
print(f"shallow_copy['settings']['timeout'][0]: {shallow_copy['settings']['timeout'][0]} → 同步修改(内层共享)")

print("\n=== 3. 深拷贝 ===")
print(f"deep_copy['app']: {deep_copy['app']} → 未修改")
print(f"deep_copy['settings']['log']['level']: {deep_copy['settings']['log']['level']} → 未修改(完全独立)")
print(f"deep_copy['settings']['timeout'][0]: {deep_copy['settings']['timeout'][0]} → 未修改(完全独立)")

4.2 核心区别总结表

特性维度赋值(引用传递)浅拷贝(copy())深拷贝(deepcopy())
内存地址与原对象完全相同外层不同,内层相同所有层级均不同
修改外层可变元素原对象同步变化原对象不变原对象不变
修改内层可变元素原对象同步变化原对象同步变化原对象不变
性能开销无(仅复制引用)小(仅复制外层)大(递归复制所有层级)
适用场景仅读数据,不修改单层对象/内层不可变多层嵌套可变对象
典型案例函数传参(仅读)单层列表去重嵌套配置文件修改

5.实战避坑:5个高频场景的正确拷贝方式

场景1:函数参数避免修改外部数据

函数传参本质是“引用传递”,若参数是可变对象,直接修改会影响外部数据。此时需根据对象复杂度选择浅拷贝或深拷贝。

import copy

def safe_modify(data):
    # 若data是单层对象,用浅拷贝
    # data_copy = data.copy()
    # 若data是嵌套对象,用深拷贝
    data_copy = copy.deepcopy(data)
    data_copy.append("modified")  # 修改拷贝后的对象
    return data_copy

# 测试嵌套列表
original = [1, 2, [3, 4]]
modified = safe_modify(original)
print(f"原列表:{original} → 未修改")  # 输出:[1,2,[3,4]]
print(f"修改后列表:{modified} → 已修改")  # 输出:[1,2,[3,4],"modified"]

场景2:配置文件的个性化修改

项目中常需基于“默认配置”修改个性化配置,若直接赋值会污染默认配置,需用深拷贝。

import copy

# 默认配置(多层嵌套)
DEFAULT_CONFIG = {
    "db": {
        "host": "localhost",
        "port": 3306,
        "params": {"charset": "utf8"}
    },
    "timeout": 30
}

# 个性化配置:基于默认配置修改,不污染原配置
user_config = copy.deepcopy(DEFAULT_CONFIG)
user_config["db"]["host"] = "192.168.1.100"  # 修改数据库地址
user_config["db"]["params"]["charset"] = "utf8mb4"  # 修改内层参数

print(f"默认配置db.host:{DEFAULT_CONFIG['db']['host']} → 仍为localhost")
print(f"用户配置db.host:{user_config['db']['host']} → 192.168.1.100")

场景3:列表去重(单层对象,浅拷贝足够)

列表去重无需修改内层数据,用浅拷贝即可,性能更高。

def deduplicate(lst):
    # 浅拷贝:先复制列表,再去重(用集合去重后转列表)
    return list(set(lst.copy()))

original = [1, 2, 2, 3, 3, 3]
unique_lst = deduplicate(original)
print(f"原列表:{original} → 未修改")
print(f"去重后列表:{unique_lst} → [1,2,3]")

场景4:性能敏感场景的“手动部分拷贝”

若数据量大且仅需修改某一层级,手动复制该层级比深拷贝更高效(避免递归复制所有数据)。

# 复杂数据:外层列表+1000个内层字典(仅需修改第1个内层字典)
big_data = [{"id": i, "value": i*10} for i in range(1000)]

# 手动部分拷贝:仅复制需要修改的内层字典,其他共享(性能高)
modified_data = big_data.copy()  # 浅拷贝外层
modified_data[0] = {"id": 0, "value": 999}  # 替换第1个内层字典(创建新对象)

print(f"原数据第1个元素:{big_data[0]} → 未修改")  # 输出:{"id":0,"value":0}
print(f"修改后第1个元素:{modified_data[0]} → 已修改")  # 输出:{"id":0,"value":999}

场景5:避免“默认参数陷阱”

函数默认参数若为可变对象(如def func(lst=[])),会导致多次调用共享同一对象,需用None+深拷贝规避。

import copy

# 错误写法:默认参数是可变对象,多次调用共享
def add_item_wrong(item, lst=[]):
    lst.append(item)
    return lst

print(add_item_wrong(1))  # 输出:[1]
print(add_item_wrong(2))  # 输出:[1,2](错误:共享列表)

# 正确写法:用None+深拷贝,每次调用创建新对象
def add_item_correct(item, lst=None):
    if lst is None:
        lst = []
    lst_copy = copy.deepcopy(lst)  # 若lst是嵌套对象,用深拷贝
    lst_copy.append(item)
    return lst_copy

print(add_item_correct(1))  # 输出:[1]
print(add_item_correct(2))  # 输出:[2](正确:独立列表)

总结:3步选择正确的拷贝方式

遇到“是否需要拷贝”的问题时,按以下3步决策,可避免99%的陷阱:

  1. 判断是否需要修改数据

    • 仅读取数据,不修改:直接赋值(无开销);
    • 需要修改数据,且不影响原对象:必须拷贝。
  2. 判断对象是否嵌套

    • 单层对象(无内层可变对象):浅拷贝(copy()/切片,性能高);
    • 多层嵌套对象(含内层可变对象):深拷贝(deepcopy(),完全独立)。
  3. 判断性能是否敏感

    • 数据量小/嵌套浅:深拷贝(方便);
    • 数据量大/嵌套深:手动部分拷贝(仅复制需要修改的层级,性能高)。

最终口诀
“只读不拷,单层浅拷,嵌套深拷,量大手拷”

通过理解变量的“引用本质”和拷贝的“层级差异”,你就能精准控制数据的独立性,避开“改一个影响另一个”的隐性bug,写出更健壮、更高效的Python代码。

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/Pocker_Spades_A/article/details/151230403

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--