Python可变对象与不可变对象深度解析:底层原理与函数传参机制
本文深入解析Python中可变对象(列表、字典)与不可变对象(字符串、数值)的区别。通过剖析List引用数组、Dict稀疏数组及字符串紧凑数组的底层实现,阐明内存地址变化规律。同时详解函数传参时的值传递与引用传递机制,揭示常见赋值与修改陷阱,帮助开发者避免细节错误。
在编写Python程序时,若对可变对象和不可变对象的理解不够深入,极易引发各类细节错误。以下通过具体代码示例进行说明:
ab = {'a':1, 'b':2}
list1 = []
for i in range(2,5):
ab['a'] = i
list1.append(ab)
print(list1) # [{'a': 4, 'b': 2}, {'a': 4, 'b': 2}, {'a': 4, 'b': 2}]
这段代码的预期结果看似应为[{‘a': 2, ‘b': 2}, {‘a': 3, ‘b': 2}, {‘a': 4, ‘b': 2}],但实际输出中列表内所有字典的键a的值均变为最后一次迭代的值4。这一现象直接关联到Python中可变对象与不可变对象的核心概念。
首先,我们需要明确什么是对象?
在Python中,万物皆对象。每个对象都具备三个基本属性:内存地址、数据类型以及存储的值。
以语句 a=5 为例,这实际上是一个创建对象并建立引用的过程。Python首先在内存中创建一个值为5的对象,该对象拥有独立的内存地址空间,随后变量a指向(引用)该对象。
接下来探讨可变对象与不可变对象的定义。
若对象在值发生变化时,其内存地址保持不变,则该对象属于可变类型。
若对象在值发生变化时,其内存地址随之改变,则该对象属于不可变类型。
众所周知,Python中的常见可变对象包括:列表(List)、字典(Dict)和集合(Set)。
而常见的不可变对象包括:元组(Tuple)、字符串(String)和数值类型(Number)。
以下代码示例能更直观地解释可变对象与不可变对象的行为差异:
当Python引用不可变对象时,会检查该对象是否已存在。若对象已创建,变量将直接引用该现有对象,而不会申请新的内存空间。
a = 5
b = 5
# 此时a和b都引用了对象5,所以地址一样
print(id(a), id(b)) # 1662825664 1662825664
# 对象发生了变化,a改变了引用,地址也发生了变化
a = 6
print(id(a), id(b)) # 1662825696 1662825664
当引用可变对象时,若对象值发生修改,其原始内存地址保持不变。
list1 = [1,2,3,4]
list2 = [1,2,3,4]
print(id(list1), id(list2)) #1754039591880 1754040417288
list1.append(5)
print(id(list1), id(list2)) #1754039591880 1754040417288
需注意:若执行 list2 = list1,则list1和list2将指向相同的内存地址。这仅是为同一对象赋予了不同的变量名。
list1 = [1,2,3,4]
list2 = list1
print(id(list1), id(list2)) #2272617112520 2272617112520
list1.append(5)
print(id(list1), id(list2)) # 2272617112520 2272617112520
那么,为何列表是可变的,而字符串或数值型是不可变的?这需要深入探究Python数据类型的底层实现机制。
List的底层实现机制。
List通过引用数组来存储列表元素。
简而言之,List在内存中开辟了一块连续的空间,用于存储指向各个元素的指针(地址)。因此,List中存储的是元素的地址,而非元素的具体值。

Dict的底层实现机制。
Dict通过稀疏数组(散列表)来实现值的存储与访问。
1. 字典的创建过程。
- 首先创建一个散列表(即稀疏数组,支持动态扩容)。
- 调用hash()函数计算键的散列值。
- 根据计算出的散列值,确定该键值对在散列表中的存储位置。
- 在确定的位置上存入对应的值。
2. 字典的访问过程。
- 计算要访问的键的散列值。
- 根据散列值及特定规则,确定该键在散列表中的位置。
- 读取该位置存储的值并返回。
字符串的底层实现机制。
字符串通过紧凑数组实现存储。
字符串数据在内存中连续存放,具有较高的空间利用率。因此,字符串被设计为不可变类型。
原因在于:每个字符的大小固定,导致字符串的总大小也是固定的。系统可以预先分配一个固定大小的空间给字符串,从而保证不可变性。
此外,补充关于函数传递参数方式的说明。
值传递机制。
当主函数向被调用函数传递不可变类型的参数时,实际上是将实参的拷贝(即临时副本)传递给被调用函数。并非传递实参本身,因此被调用函数无法直接修改主调函数中变量的值,只能修改其私有的临时副本。
引用传递机制。
当主函数向被调用函数传递可变类型的参数时,实际上是将实参的引用(地址)传入调用函数。对引用的操作等同于对原对象的操作。
需注意以下两种常见情况:
list1 = [1,2,3,4]
def solution(list1):
list1 = [1,2,3,4,5]
return list1
solution(list1)
print(list1) # [1,2,3,4]
list1 = [1,2,3,4]
def solution(list1):
list1.append(5)
return list1
solution(list1)
print(list1) # [1,2,3,4,5]
第一种情况:若在函数内部使用“=”进行赋值,这相当于重新创建了一块内存空间存放新对象,并将局部变量list1指向该新对象。因此,全局作用域中的list1并未发生改变。
第二种情况:若使用append方法,即直接修改原对象的值,这属于对原对象的操作,因此全局列表会被修改。
参考:
Python 类、对象、数据分类、函数参数传递的理解
一篇文章教你掌握python数据类型的底层实现
总结
本文到此结束,希望能为读者提供帮助。欢迎持续关注本站,获取更多技术内容!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









