Python - 用C指针理解Python的可变类型与不可变类型的底层原理

先来看一段代码:

list = [1,2,3,4,5]

list_copy = list
list_copy[0] = 2

print(list)
print(list_copy)

-----输出结果------
# list 
[2,2,3,4,5]

# list_copy
[2,2,3,4,5]

如果不从指针的角度出发,上述代码的逻辑似乎正确:我先创建一个list,再拷贝一份副本list_copy,然后再对副本进行修改。

但是结果却是对“副本”的修改导致了“主本”的原数据也发生了变化。这种错误常见于:当得到了某个对象,希望对其内部某个值进行操作,但又不想令其本身发生变化时,或许就会步入这种“临时拷贝副本”的语法错误中。

于是可以引出本文的主题:Python的可变类型与不可变类型。

这里先放结论可变类型指内存内容可以被原地修改的数据类型,不可变类型指内存内容不可以被原地修改的数据类型。

常见的可变类型:列表list, 字典dictionary, 集合set

常见的不可变类型:字符串str, 整型int, 浮点型float, 元组tuple.

要深入理解这个问题,必须引入内存的概念。程序都是会占用内存空间的,我们在程序内所定义的变量也存放在内存中。用上面的代码举例来说我们在内存中开辟了一块地方,用于存放二进制信息,而这些二进制信息被解析之后就是列表[1,2,3,4,5]。

而list = [1,2,3,4,5]中的list是什么呢?我们可以用C语言中的指针来类比它。

我们刚刚不是说我们在内存中开辟了一块地方用来存储[1,2,3,4,5]吗?那么list就是一个指向这块“地方”的“指针”,即它记录了这块“地方”的地址。

因此,每当我们print(list)的时候,我们并不是打印list,而是打印它所指向的那块内存地址所存储的信息。如图:

那么我们上面“拷贝副本”的操作实际上是干了些什么呢?如图:

实际上,“list=list_copy” 这样的操作,并没有拷贝出一份“副本”。

我们只是创建了另一个“指针”list_copy,它和list都指向同一块内存空间

好了,回到最初的结论:可变类型指内存信息可以被原地修改的数据类型。[1,2,3,4,5]是列表,属于可变类型。

当我们进行“list_copy[0] = 2”这样的操作时,实际上是通过操作list_copy这个“指针”,把[1,2,3,4,5]原地修改为了[2,2,3,4,5],这类似于C语言中对指针进行的“解引用”操作。

显而易见,此时我们无论打印list还是list_copy,出来的结果一定都是[2,2,3,4,5],因为[1,2,3,4,5]已经不存在了。

那么对于不可变类型呢?我们再引入一段代码:

a = '12345'
print(a) 
>> '12345'

a = '22345'
print(a) 
>> '22345'

这里我们似乎修改了a的值,但实际上在内存中的操作是怎样的呢?如图:

字符串str为不可变类型,因此我们实际上并没有在内存中修改'12345',而是新开辟了一块空间用于存放'22345',再让原先的指针a指向这块新的内存地址。

我们可以通过id(a)来验证这一点:id()的输出值为a所指向的内存地址,即存放字符串二进制信息的地方。

a = '12345'
print(id(a))
>> 1996319069552

a = '22345'
print(id(a))
>> 1996319069680

如何避免对可变类型的这种错误操作?

很简单,错误的根本原因是list_copy与list都指向了同一块内存地址。那么我们只要让他们分别指向不同的内存地址就可以了:一种可行的方法是——切片操作

list = [1,2,3,4,5]
list_copy = list[:]
list_copy[0] = 2

print(list)
[1, 2, 3, 4, 5]
print(list_copy)
[2, 2, 3, 4, 5]

“list_copy = list[:]”,代表我们先对list进行了切片操作,并且为切片结果新开辟一块内存空间,再让list_copy指向这块新的内存空间。

我们同样通过id()来验证我们的说法:

list = [1,2,3,4,5]
list_copy = list[:]

print(id(list))
>> 2427089033280

print(id(list_copy))
>> 2427092413184

如图:

现在list_copy与list分别指向了不同的内存空间,因此无论我们对list_copy做怎样的操作,都不会影响到list所指向的内存空间。

注意:对“指针”的重新赋值不会导致原指向值的改变。

还是用上面的例子说明这里的意思:虽然list_copy和list都指向同一内存空间,但如果我们不是通过

list_copy[0] = 2

令其指向的内存空间发生变化,而是对它重新赋值(通过“=”号)以令其指向其它内存空间,那么其原先所指向的值也不会发生变化。例如:

list_cooy = list+[6]
list = [1 ,2 ,3 ,4 ,5]
list_copy = list

id(list)
>> 2652738720960
id(list_copy)
>> 2652738720960
# 此时list与list_copy都指向同一内存空间

list_copy = list +[6]
# 对list_copy进行重新赋值,让它指向另一块内存空间,而这块内存空间所存储的信息是原先list所指向空间的内容的拷贝+[6]

id(list)
>> 2652738720960
id(list_copy)
>> 2652739773504
# 此时验证list_copy已经指向了另一内存空间,而原list的指向无变化。

print(list)
>> [1, 2, 3, 4, 5]
print(list_copy)
>> [1, 2, 3, 4, 5, 6]

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值