Python可变对象与不可变对象,以及深拷贝与浅拷贝

本文详细介绍了Python中可变对象与不可变对象的区别,以及函数默认参数的陷阱。重点讲解了深拷贝与浅拷贝在处理可变对象时的不同,并通过实例代码展示了它们的运作机制。理解这些概念对于避免程序中的意外行为至关重要。
摘要由CSDN通过智能技术生成

Python可变对象与不可变对象,以及深拷贝与浅拷贝

本文主要内容为Python中可变对象以及不可变对象的概念,以及使用深拷贝与浅拷贝复制对象的区别。所有代码都基于Python3.8运行。

可变对象(mutable objects)与不可变对象(immutable objects)

首先来看一段代码:

class StrangeList:
    def __init__(self, init_list=[]):
        self._list = init_list
       
    def add(self, element):
        self._list.append(element)
     
    def __repr__(self):
        return f'{self._list}'
        
for num in range(3):
    some_list = StrangeList()
    some_list.add(num)
    print(some_list)
    
# 运行结果为
[0]
[0, 1]
[0, 1, 2]

和很多人想的可能不一样,输出的结果并非是[0],[1],[2]。首先我们来看官方文档的解释:

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function.

这里的知识点有两个:Python中函数/方法默认参数的初始化方式以及可变对象的特点。简单来说,Python的函数/方法的默认参数只会在定义时初始化一次,后续使用都是直接从内存地址中获取它使用,而非每次都在内存中生成一个新的参数。而这便会导致当默认参数为可变对象时,如果函数内对该对象的值改变了,默认参数的值也会跟着改变,从而影响下一次调用的结果。

彻底理解以上这段话需要弄清楚可变对象和不可变对象的概念。在Python中,对象分为可变对象和不可变对象两种。

可变对象包括:dict, set, list。不可变对象包括:str, int, string, float ,tuple。

最直观的区别,当我们改变可变对象内部的值时,该变量的id不会变。而当改变不可变对象的值时,该变量的id会发生改变。

这是因为在Python中,我们将某个对象赋值给一个变量时,其实是将该对象的内存地址赋值给变量。所以在后续修改变量的值时,Python会根据对象的类型不同而使用不同的赋值方式。

如果是不可变对象,在修改变量时,Python会开辟一块新的内存地址赋值存放对应的值,然后将变量指向这个新地址。因此修改变量前后的id(var)是会改变的。

而如果是可变对象,在修改变量时,并不会改变变量所指向的地址,而是在原来的地址中直接对值进行修改,因此id(var)在修改前后并不会改变。

参考以下代码:

immu_a = 1234
print(f'immu_a is : {id(immu_a)}')
immu_a = '2'
print(f'immu_a is : {id(immu_a)}')
mu_a = [1]
print(f'mu_a is : {id(mu_a)}')
mu_a += [0]
print(f'mu_a is : {id(mu_a)}')
mu_a[0] = 2
print(f'mu_a is : {id(mu_a)}')
mu_a = [1, 2, 3]
print(f'mu_a is : {id(mu_a)}')

#输出结果
immu_a is : 1565367873744
immu_a is : 1564823327600
mu_a is : 1565367847936
mu_a is : 1565367847936
mu_a is : 1565367847936
mu_a is : 1565367898624

可以看到对不可变对象的修改造成了id的改变,而对可变对象的修改没有造成id变化。值得注意的是最后一句输出,id已经发生了变化。这是因为mu_a = [1, 2, 3]这条语句并不是对值修改,而是将mu_a重新赋值了,所以最后输出的id发生了变化。

现在让我们回到开头那段代码,由于init_list是一个可变对象,for循环中每一次调用add方法都对init_list地址中的值发生了改变,而每一次使用__init__方法初始化对象是其实使用的都是同一个地址,这就造成了默认参数的值被污染的情况。

而如果如果默认参数是不可变对象则不会有这个问题,因为每次对它的值改变时,实际上变量已经指向了一个新的地址,不会影响默认参数地址中的值。

测试代码以及结果:


def test(i,var='immutable'): 
print(id(var))
var += str(i)
print(id(var))
print(var)

for i in range(3):
    test(i)

# 输出结果
2263406660976
2262862248752
immutable0
2263406660976
2262862248752
immutable1
2263406660976
2262862248752
immutable2

深拷贝与浅拷贝

在理解可变对象和不可变对象后,我们就可以理解Python中的深拷贝(deepcopy)和浅拷贝(copy)了。

先看代码

a = [1, 2, 3]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)
print(id(a), id(b), id(c), id(d))

# 运行结果为:
2922340283400 2922340283400 2922339315464 2922340284872

在上述代码中,a和b指向的是同一个内存地址,因此在修改其中一个变量值时,会对另外一个变量的结果干扰。而使用了deepcopy和copy的c,d内存地址与a互不相同,说明copy和deepcopy会将值复制到一个新的内存地址中。

那deepcopy和copy的区别又在哪,继续看以下代码

a = [1, [1, 2, 3], 3]
b = ac = copy.copy(a)
d = copy.deepcopy(a)
print(id(a[1]), id(b[1]), id(c[1]), id(d[1]))
print(id(a[1][3]), id(c[1][3]), id(d[1][3]))

# 运行结果为:
第二层id1987393610824 1987393610824 1987393610824 1987394936904
第三层id2711129158664 2711129158664 2711129159304

我们可以看到浅拷贝只是对第一层值的拷贝,而里面嵌套的值还是使用原本的地址。而深拷贝则会完全复制一份原本变量的值,并且在内存中开辟新的空间。

思考:以下代码中输出的三个地址是否会相等

a = [1, [1, 2, 3]]
b = copy.copy(a)
c = copy.deepcopy(a)
print(id(a[1][1]), id(c[1][1]), id(c[1][1]))

参考文章:
https://docs.python.org/3/reference/compound_stmts.html#function-definitions
https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值