Python中的切片拷贝和可变参数传参
1.几种拷贝方式的比较
from copy import deepcopy
a = [1,2,[3,4,5]]
a1 = a #直接赋值,传引用
a2 = a.copy() #shallow copy
a3 = deepcopy(a) #deep copy
a4 = a[:] #slice: shallow copy
a[0] = 6 #改变原对象中的可变类型和不可变类型
a[2][0] = 7
# print(a)
# print(a1)
# print(a2)
# print(a3)
# print(a4)
#输出结果:
# [6, 2, [7, 4, 5]]
# [6, 2, [7, 4, 5]] #直接传引用,导致原对象和引用对象指向同一片内存区域,修改同步
# [1, 2, [7, 4, 5]] #浅拷贝,对于不可变对象,修改时会改变对应元素,但可变对象不会
# [1, 2, [3, 4, 5]] #深拷贝,递归地拷贝内部所有元素,因而是独立部分
# [1, 2, [7, 4, 5]] #这里切片实现的是 浅拷贝
可以注意到,直接的赋值语句是传递引用,进行变量关联,而List 本身的copy() 方法是实现浅拷贝的工作。
2. 切片赋值
对于切片赋值,请看原理:
list 本身会有__setitem__的方法,查看是不是传入slice对象(isinstance),如果是slice对象,通过slice对象的start,stop,step索引值进行赋值
本质上,容器类型,都会有对应的__getitem__ / __setitem__的方法,get/set对应的需求的值。
https://github.com/python/cpython/blob/5c22476c01622f11b7745ee693f8b296a9d6a761/Objects/listobject.c#L697
这里就可以发现,实际上是对于源数组本身的对象地址开始赋值(个人猜测,memcpy是体现深拷贝的地方)。
验证这种方法对于右端待复制值是深拷贝:
A = [1,2,[3,4,5]]
B = [6,7,8]
A[0:2] = B
print(A)
print(B)
B[0] = 9
print(A)
print(B)
# 输出结果:
# [6, 7, 8, [3, 4, 5]]
# [6, 7, 8]
# [6, 7, 8, [3, 4, 5]]
# [9, 7, 8]
总结:使用A[:]=B的格式时,会对右侧对象进行深拷贝,通过内部方法实现对于slice对应的部分进行设置。
3. 其他惊喜发现
为什么Python的slice本身是浅拷贝?
https://github.com/python/cpython/blob/5c22476c01622f11b7745ee693f8b296a9d6a761/Objects/listobject.c#L487
这里的for循环对于src和dect的赋值,看起来是浅拷贝的形式,没有发现对应的深拷贝。
4. 函数内部的可变对象传参
# -*- coding: utf-8 -*-
"""
Created on Wed Feb 16 19:09:32 2022
@author: dj07911@163.com
"""
def change_assign(a):
res = []
res.append(1)
a = res
def change_slice_assign(a):
res = []
res.append(3)
a[:] = res
a = [1,2]
change_assign(a)
print(a) #输出的是原来的a,没有经过改变
change_slice_assign(a)
print(a) #输出的是改变的a,因为可变类型传参,更像是C++里面的引用传参机制
在编辑这段代码的时候,IDE同时也给出了提示信息(Spider NB)
这里的 a 已经不是作为原来传进来的 a 处理了,而是相当于新建了局部变量 a 作为 res 的引用
Python 处理变量搜索具有LEGB原则,参考:
Python LEGB规则 - 简书 (jianshu.com)
所以没有对 a 进行修改,导致看起来是可变参数没有修改,而实际上是生成了对象引用的本地变量,函数结束时就不存在了。
而下面的change_slice_assign则使用之前提到的切片浅拷贝赋值,实现了对于res的拷贝。
另外,补充一点 在函数中的 += 和 = 也有类似的区别问题
def change_add(a):
a = a + a
def change_iadd(a):
a += a
a = [1,2]
change_add(a)
print(a) #输出的是原来的a,没有经过改变
change_iadd(a)
print(a) #输出的是改变的a,因为+= __iadd__: in-place add
如果进行修改:
a[:] = a + a
则可以解决这一问题。