返回顶部
分享到

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

python 来源:互联网 作者:佚名 发布时间:2025-12-01 21:59:33 人浏览
摘要

在Python开发中,可变对象与不可变对象是一个高频基础概念,也是初学者容易混淆的难点。这两类对象的核心差异不仅影响变量赋值、函数传参的逻辑,更直接关系到代码的性能与安全性。本

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

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

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

1. 不可变对象(Immutable)

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

  • 基础类型:int(整数)、str(字符串)、float(浮点数)、bool(布尔值)
  • 容器类型:tuple(元组)、frozenset(冻结集合)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

# 示例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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# 示例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()函数查看)。

两类对象的关键差异:

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

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

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

1

2

3

4

5

6

7

# 初始赋值: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为例)

1

2

3

4

5

6

7

# 初始赋值: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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# 示例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)不可变对象:赋值创建新对象

1

2

3

4

5

6

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)可变对象:赋值共享引用

1

2

3

4

5

6

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)不可变对象:函数内修改不影响外部

1

2

3

4

5

6

7

8

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)可变对象:函数内修改影响外部

1

2

3

4

5

6

7

8

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()):

1

2

3

4

5

6

7

8

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")、字典的键(字典键必须不可变);
  • 多线程环境:不可变对象无需担心并发修改问题(线程安全);
  • 需要哈希的场景:如集合元素(集合元素必须可哈希,不可变对象通常可哈希)。

1

2

3

4

5

6

7

8

9

10

11

# 示例:不可变对象作为字典键(合法)

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)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

# 示例:可变对象存储动态数据

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:可变对象作为函数默认参数

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

1

2

3

4

5

6

7

# 错误示例:用列表作为默认参数

def add_item(item, lst=[]):  # lst在函数定义时初始化一次

    lst.append(item)

    return lst

 

print(add_item(1))  # 输出:[1](首次调用正常)

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

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

1

2

3

4

5

6

7

8

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:误修改共享的可变对象

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

1

2

3

4

5

6

# 问题场景:复制列表时直接赋值(共享引用)

user_list = ["张三", "李四", "王五"]

admin_list = user_list  # admin_list与user_list指向同一列表

 

admin_list.remove("王五")  # 修改admin_list

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

# 方案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:混淆“不可变对象的修改”与“重新赋值”

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

1

2

3

4

5

6

7

# 错误示例:直接修改字符串

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

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

1

2

3

4

5

6

7

8

9

# 正确:字符串重新赋值

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,适合存储动态数据,操作高效但需注意共享引用问题。

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

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

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


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • python可变/不可变对象及+=和=+举例
    在Python开发中,可变对象与不可变对象是一个高频基础概念,也是初学者容易混淆的难点。这两类对象的核心差异不仅影响变量赋值、函数
  • Python使用Spire.XLS for Python实现高效读取Excel数据
    在当今数据驱动的世界中,Python 已成为数据处理和分析的首选工具。而 Excel 文件作为最常见的数据存储格式之一,如何高效、准确地在 P
  • 安装scrapy框架并测试全过程

    安装scrapy框架并测试全过程
    安装scrapy框架并测试 这是个系列文章,主要是能让大家快速的的做出一个小项目,主要是我现在在做计算机设计大赛,想把做过的东西记录
  • Python实现PDF文档高效转换为HTML文件
    一、为什么需要PDF转HTML 在数字化办公场景中,PDF因其格式固定、跨平台兼容性强成为文档分发的主流格式。但PDF的静态特性限制了内容复用
  • Python使用Appium实现自动化操作手机入门教学

    Python使用Appium实现自动化操作手机入门教学
    在当今移动互联网时代,手机应用已经成为人们日常生活中不可或缺的一部分。随着移动应用的快速发展,自动化测试和手机操作的需求也
  • 利用Playwright实现文件上传与下载的完成判断
    在自动化测试或网页数据交互场景中,文件上传与下载是极为常见的操作。Playwright 作为强大的自动化测试工具,不仅能模拟用户触发上传和
  • python学习必备知识
    一、变量 1.变量 指在程序执行过程中,可变的量; 定义一个变量,就会伴随有3个特征,分别是内存ID、数据类型和变量值。 其他语言运行
  • Python PiP换镜像源的实现

    Python PiP换镜像源的实现
    1、更新PiP 1 python -m pip install --upgrade pip 2、永久换源(阿里) 1 pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ 3、临时换源(阿里)
  • Python实现专业级字符串清理技术的完全指南

    Python实现专业级字符串清理技术的完全指南
    引言:数据清洗的核心挑战 在数据处理领域,超过80%的时间都花在数据清洗上,而字符串净化是其中最关键的一环。根据2023年数据工程报告
  • Python ttk模块简介与使用
    Python编程之ttk模块详细介绍与使用教程 1. ttk 简介 ttk (Themed Tkinter) 是 Python 标准库中 tkinter 的一个扩展模块,提供了更加现代化、主题化的
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计