Python可变对象与不可变对象深度解析:底层原理与函数传参机制

晚来风
晚来风
发布时间:2026-10-15 | 2人浏览过
原创

本文深入解析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数据类型的底层实现

总结

本文到此结束,希望能为读者提供帮助。欢迎持续关注本站,获取更多技术内容!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表