当前位置: 代码网 > it编程>前端脚本>Python > python可变/不可变对象及+=和=+举例详解

python可变/不可变对象及+=和=+举例详解

2025年12月01日 Python 我要评论
前言在python开发中,“可变对象”与“不可变对象”是一个高频基础概念,也是初学者容易混淆的难点。这两类对象的核心差异不仅影响变量赋值、函数传参的逻辑

前言

在python开发中,“可变对象”与“不可变对象”是一个高频基础概念,也是初学者容易混淆的难点。这两类对象的核心差异不仅影响变量赋值、函数传参的逻辑,更直接关系到代码的性能与安全性。本文将从定义区分→底层原理→核心差异→实战场景四个维度,帮你彻底搞懂这两类对象,避免开发中因概念模糊导致的bug。

一、先明确:什么是可变对象?什么是不可变对象?

首先用最通俗的语言定义两类对象,再通过示例直观感受差异——核心区别在于“对象创建后,能否修改其内部数据”。

1. 不可变对象(immutable)

定义:对象创建后,其内部数据(值)无法被修改,若要“修改”,只能创建一个新对象并指向新地址。
python中常见的不可变对象:

  • 基础类型:int(整数)、str(字符串)、float(浮点数)、bool(布尔值)
  • 容器类型:tuple(元组)、frozenset(冻结集合)
# 示例1:int类型(不可变)
a = 10
print(f"修改前a的值:{a},地址:{id(a)}")  # 输出:修改前a的值:10,地址:140708484554720

a = a + 2  # 看似“修改”,实际创建新对象
print(f"修改后a的值:{a},地址:{id(a)}")  # 输出:修改后a的值:12,地址:140708484554784(地址变化)

# 示例2:str类型(不可变)
s = "python"
print(f"修改前s的值:{s},地址:{id(s)}")  # 输出:修改前s的值:python,地址:2524607223408

s = s.replace("p", "p")  # replace()返回新字符串,原对象不变
print(f"修改后s的值:{s},地址:{id(s)}")  # 输出:修改后s的值:python,地址:2524607223664(地址变化)

# 示例3:tuple类型(不可变)
t = (1, 2, 3)
# t[0] = 100  # 报错:typeerror: 'tuple' object does not support item assignment(无法修改元素)

2. 可变对象(mutable)

定义:对象创建后,其内部数据(值)可以被直接修改,且修改后对象的地址(身份标识)保持不变。
python中常见的可变对象:

  • 容器类型:list(列表)、dict(字典)、set(集合)
  • 其他:bytearray(字节数组)、自定义类实例(默认可变)
# 示例1:list类型(可变)
lst = [1, 2, 3]
print(f"修改前lst的值:{lst},地址:{id(lst)}")  # 输出:修改前lst的值:[1,2,3],地址:2524607215232

lst.append(4)  # 直接修改列表内部数据
print(f"修改后lst的值:{lst},地址:{id(lst)}")  # 输出:修改后lst的值:[1,2,3,4],地址:2524607215232(地址不变)

# 示例2:dict类型(可变)
d = {"name": "张三", "age": 25}
print(f"修改前d的值:{d},地址:{id(d)}")  # 输出:修改前d的值:{'name':'张三','age':25},地址:2524607214848

d["age"] = 26  # 直接修改字典value
print(f"修改后d的值:{d},地址:{id(d)}")  # 输出:修改后d的值:{'name':'张三','age':26},地址:2524607214848(地址不变)

# 示例3:set类型(可变)
s = {1, 2, 3}
print(f"修改前s的值:{s},地址:{id(s)}")  # 输出:修改前s的值:{1,2,3},地址:2524607198976

s.add(4)  # 直接修改集合内部数据
print(f"修改后s的值:{s},地址:{id(s)}")  # 输出:修改后s的值:{1,2,3,4},地址:2524607198976(地址不变)

二、底层原理:为什么会有“可变”与“不可变”之分?

两类对象的差异本质是内存存储机制对象身份标识(id) 的设计不同,核心在于“修改操作是否改变对象的内存地址”。

1. 核心概念:id、value、type

在python中,每个对象都有三个核心属性:

  • id:对象的唯一身份标识,对应内存地址(通过id()函数查看);
  • value:对象的实际数据(如10"python"[1,2,3]);
  • type:对象的类型(通过type()函数查看)。

两类对象的关键差异:

  • 不可变对象idvalue绑定,value一旦确定,id就固定;修改value必须创建新对象(新id);
  • 可变对象id与“容器本身”绑定,value(容器内的数据)可修改,且修改后id不变。

2. 内存存储示意(直观理解)

(1)不可变对象(以int为例)

# 初始赋值:a指向id=140708484554720的对象(value=10)
a = 10
内存:a → [id=140708484554720, value=10, type=int]

# “修改”操作:创建新对象(id=140708484554784,value=12),a重新指向新对象
a = a + 2
内存:a → [id=140708484554784, value=12, type=int](原对象10仍存在,等待垃圾回收)

(2)可变对象(以list为例)

# 初始赋值:lst指向id=2524607215232的列表对象(内部存储[1,2,3])
lst = [1, 2, 3]
内存:lst → [id=2524607215232, value=[1,2,3], type=list]

# 直接修改:列表内部数据变为[1,2,3,4],但lst仍指向原id
lst.append(4)
内存:lst → [id=2524607215232, value=[1,2,3,4], type=list](id不变,仅value修改)

3. 不可变对象的“缓存机制”(额外知识点)

python对部分不可变对象(如小整数、短字符串)有缓存机制,即重复创建相同值的对象时,会复用已有的对象(避免频繁创建销毁,节省内存)。

# 示例1:小整数缓存(范围通常是-5~256)
a = 10
b = 10
print(id(a) == id(b))  # 输出:true(复用同一对象)

c = 1000
d = 1000
print(id(c) == id(d))  # 输出:false(超出小整数范围,创建新对象)

# 示例2:短字符串缓存(字符串驻留机制)
s1 = "python"
s2 = "python"
print(id(s1) == id(s2))  # 输出:true(复用同一对象)

s3 = "python123"
s4 = "python123"
print(id(s3) == id(s4))  # 输出:true(短字符串通常会被缓存)

s5 = "python " + "123"  # 动态拼接的字符串,是否缓存取决于解释器
print(id(s5) == id(s3))  # 输出:false(动态拼接未触发缓存)

注意:缓存机制是python的内部优化,开发者不应依赖此特性(如不能通过id判断两个不可变对象的值是否相等,应直接用==比较值)。

三、核心差异对比:从赋值、传参到使用场景

两类对象在变量赋值函数传参使用场景上的差异,是开发中最容易踩坑的地方,用表格直观对比:

对比维度

不可变对象(如int、str、tuple)

可变对象(如list、dict、set)

赋值逻辑

新赋值会创建新对象,变量指向新id

新赋值仅让变量指向原对象(共享引用),修改内部数据会影响所有引用

函数传参

传“值的引用”,函数内修改不会影响外部变量

传“对象的引用”,函数内修改会影响外部对象

==is判断

==比较值,is比较id(缓存可能导致is为true)

==比较值,is比较id(修改后is仍为true)

安全性

线程安全(不可修改,无并发修改风险)

非线程安全(多线程修改需加锁)

适用场景

存储固定数据(如配置、常量、字典键)

存储动态数据(如待处理列表、实时更新的字典)

1. 变量赋值:共享引用 vs 独立对象

(1)不可变对象:赋值创建新对象

a = 10
b = a  # b指向a的对象(id相同)
print(f"a: {a}, id(a): {id(a)}; b: {b}, id(b): {id(b)}")  # 输出:a:10, id(a):140708484554720; b:10, id(b):140708484554720

a = a + 2  # a指向新对象(id变化)
print(f"a: {a}, id(a): {id(a)}; b: {b}, id(b): {id(b)}")  # 输出:a:12, id(a):140708484554784; b:10, id(b):140708484554720(b不受影响)

(2)可变对象:赋值共享引用

lst1 = [1, 2, 3]
lst2 = lst1  # lst2与lst1指向同一对象(共享引用)
print(f"lst1: {lst1}, id(lst1): {id(lst1)}; lst2: {lst2}, id(lst2): {id(lst2)}")  # 输出:lst1:[1,2,3], id(lst1):2524607215232; lst2:[1,2,3], id(lst2):2524607215232

lst1.append(4)  # 修改lst1(共享对象)
print(f"lst1: {lst1}, id(lst1): {id(lst1)}; lst2: {lst2}, id(lst2): {id(lst2)}")  # 输出:lst1:[1,2,3,4], id(lst1):2524607215232; lst2:[1,2,3,4], id(lst2):2524607215232(lst2同步变化)

2. 函数传参:传值 vs 传引用(python的“传对象引用”机制)

python的函数传参既不是纯“传值”,也不是纯“传引用”,而是 “传对象引用” ——本质是将变量指向的对象地址(id)传给函数参数,参数与原变量共享同一对象。

(1)不可变对象:函数内修改不影响外部

def modify_immutable(x):
    x = x + 10  # 创建新对象,参数x指向新id
    print(f"函数内x: {x}, id(x): {id(x)}")

a = 5
print(f"调用前a: {a}, id(a): {id(a)}")  # 输出:调用前a:5, id(a):140708484554560
modify_immutable(a)  # 输出:函数内x:15, id(x):140708484554880
print(f"调用后a: {a}, id(a): {id(a)}")  # 输出:调用后a:5, id(a):140708484554560(a不受影响)

(2)可变对象:函数内修改影响外部

def modify_mutable(lst):
    lst.append(10)  # 直接修改共享对象
    print(f"函数内lst: {lst}, id(lst): {id(lst)}")

lst = [1, 2, 3]
print(f"调用前lst: {lst}, id(lst): {id(lst)}")  # 输出:调用前lst:[1,2,3], id(lst):2524607215232
modify_mutable(lst)  # 输出:函数内lst:[1,2,3,10], id(lst):2524607215232
print(f"调用后lst: {lst}, id(lst): {id(lst)}")  # 输出:调用后lst:[1,2,3,10], id(lst):2524607215232(lst被修改)

避坑技巧:若想避免函数修改外部可变对象,可在函数内创建对象的副本(如lst.copy()dict.copy()copy.deepcopy()):

def modify_mutable_safe(lst):
    new_lst = lst.copy()  # 创建副本,修改副本不影响原对象
    new_lst.append(10)
    print(f"函数内new_lst: {new_lst}")

lst = [1, 2, 3]
modify_mutable_safe(lst)  # 输出:函数内new_lst: [1,2,3,10]
print(f"调用后lst: {lst}")  # 输出:调用后lst: [1,2,3](原对象未修改)

四、实战场景:如何选择可变/不可变对象?

两类对象没有绝对的“优劣”,只有“适用场景”的差异,开发中需根据需求选择:

1. 优先用不可变对象的场景

  • 存储固定不变的数据:如程序常量(pi = 3.14159)、配置参数(db_host = "localhost")、字典的键(字典键必须不可变);
  • 多线程环境:不可变对象无需担心并发修改问题(线程安全);
  • 需要哈希的场景:如集合元素(集合元素必须可哈希,不可变对象通常可哈希)。
# 示例:不可变对象作为字典键(合法)
config = {
    ("db", "host"): "localhost",  # 元组(不可变)作为键
    ("db", "port"): 3306,
    "timeout": 30  # 字符串(不可变)作为键
}

# 错误示例:列表(可变)不能作为字典键
invalid_config = {
    ["db", "host"]: "localhost"  # 报错:typeerror: unhashable type: 'list'
}

2. 优先用可变对象的场景

  • 存储动态变化的数据:如待处理的任务列表(tasks = [])、实时更新的用户数据(user_info = {"name": "张三", "score": 0});
  • 需要频繁修改内部数据的场景:如列表的append()remove(),字典的update()等操作(无需创建新对象,效率更高);
  • 传递复杂数据结构并允许修改:如函数间传递列表,允许函数补充数据(需明确告知修改逻辑,避免隐藏bug)。
# 示例:可变对象存储动态数据
user_scores = {"张三": 85, "李四": 92}
# 动态更新分数(无需创建新字典,直接修改)
user_scores["张三"] = 88  # 覆盖原分数
user_scores["王五"] = 79  # 新增数据
print(user_scores)  # 输出:{'张三': 88, '李四': 92, '王五': 79}

# 示例:函数间传递可变对象并协作修改
def add_task(tasks, task):
    tasks.append(task)  # 直接修改传入的列表

task_list = ["写代码", "测功能"]
add_task(task_list, "修复bug")
print(task_list)  # 输出:['写代码', '测功能', '修复bug'](原列表被更新)

五、避坑指南:这些场景最容易出错!

因对可变/不可变对象理解不清导致的bug,在开发中非常常见,以下是高频坑点及解决方案:

1. 坑点1:可变对象作为函数默认参数

问题:函数默认参数在定义时仅初始化一次,若默认参数是可变对象(如列表、字典),多次调用函数会共享该对象,导致意外累积数据。

# 错误示例:用列表作为默认参数
def add_item(item, lst=[]):  # lst在函数定义时初始化一次
    lst.append(item)
    return lst

print(add_item(1))  # 输出:[1](首次调用正常)
print(add_item(2))  # 输出:[1, 2](二次调用复用了同一个列表,意外累积)

解决方案:默认参数用none代替可变对象,在函数内初始化:

def add_item(item, lst=none):
    if lst is none:
        lst = []  # 每次调用时重新初始化列表
    lst.append(item)
    return lst

print(add_item(1))  # 输出:[1]
print(add_item(2))  # 输出:[2](符合预期)

2. 坑点2:误修改共享的可变对象

问题:多个变量引用同一可变对象时,修改其中一个变量会影响其他变量,导致数据不一致。

# 问题场景:复制列表时直接赋值(共享引用)
user_list = ["张三", "李四", "王五"]
admin_list = user_list  # admin_list与user_list指向同一列表

admin_list.remove("王五")  # 修改admin_list
print(user_list)  # 输出:['张三', '李四'](user_list也被修改,可能非预期)

解决方案:创建可变对象的副本(浅拷贝/深拷贝),避免共享引用:

# 方案1:浅拷贝(适用于元素为不可变对象的情况)
user_list = ["张三", "李四", "王五"]
admin_list = user_list.copy()  # 列表专用拷贝
# 或 admin_list = list(user_list)
# 或 admin_list = user_list[:]

admin_list.remove("王五")
print(user_list)  # 输出:['张三', '李四', '王五'](原列表不受影响)

# 方案2:深拷贝(适用于嵌套可变对象的情况,需用copy模块)
import copy
nested_list = [1, [2, 3], 4]
shallow_copy = nested_list.copy()  # 浅拷贝:内层列表仍共享
deep_copy = copy.deepcopy(nested_list)  # 深拷贝:完全独立

shallow_copy[1].append(5)
print(nested_list)  # 输出:[1, [2, 3, 5], 4](浅拷贝影响原对象)
deep_copy[1].append(6)
print(nested_list)  # 输出:[1, [2, 3, 5], 4](深拷贝不影响原对象)

3. 坑点3:混淆“不可变对象的修改”与“重新赋值”

问题:试图直接修改不可变对象(如字符串、元组)会报错,需通过重新赋值生成新对象。

# 错误示例:直接修改字符串
s = "python"
s[0] = "p"  # 报错:typeerror: 'str' object does not support item assignment

# 错误示例:直接修改元组
t = (1, 2, 3)
t[0] = 100  # 报错:typeerror: 'tuple' object does not support item assignment

解决方案:通过拼接、切片等方式生成新对象,再重新赋值:

# 正确:字符串重新赋值
s = "python"
s = "p" + s[1:]  # 生成新字符串
print(s)  # 输出:python

# 正确:元组重新赋值(通过切片生成新元组)
t = (1, 2, 3)
t = (100,) + t[1:]  # 生成新元组
print(t)  # 输出:(100, 2, 3)

六、总结:理解本质,灵活运用

可变对象与不可变对象的核心差异,在于“修改操作是否改变对象的内存地址(id)”:

  • 不可变对象:修改即创建新对象(id改变),适合存储固定数据,线程安全;
  • 可变对象:修改不改变id,适合存储动态数据,操作高效但需注意共享引用问题。

掌握这一本质后,就能理解:

  • 为什么函数传参时,列表会被修改而整数不会;
  • 为什么字典的键必须是不可变对象;
  • 为什么默认参数不能用可变对象。

实际开发中,没有“必须用哪种”的绝对规则,关键是根据场景选择:需要固定数据用不可变对象,需要动态修改用可变对象,并注意规避共享引用导致的意外修改。

到此这篇关于python可变/不可变对象及+=和=+的文章就介绍到这了,更多相关python可变/不可变对象及+=和=+内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com