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

则可以解决这一问题。

posted @ 2022-02-16 23:13  Sanhao99  阅读(412)  评论(0编辑  收藏  举报