Python 赋值、深浅拷贝介绍与简要实现以及在数组扩展的潜在问题

Python 赋值、深浅拷贝介绍与简要实现以及在数组扩展的潜在问题

1· 赋值与深浅拷贝

1.1 数据的存储与类型

Python语言和其他高级语言一样,变量是对内存及其地址的抽象。Python中万物皆对象,变量存储的是其地址(引用)而非值本身。
Python的数据类型,我一般看做基本类型和复合类型。基本类型就是int, long, bool, str等,复合类型则是各种数据结构如list, tuple, dict, set等。

1.2 赋值

对于基本的数据类型,被初始化时会分配一块内存空间,由于基本数据类型的初始化都是赋值一个常量,基本数据类型的变量则对应了一个存储了常量的空间。对基本数据类型进行赋值,等价于重新开辟一块内存空间保存新的值,并将变量重新指向新的空间,如果需要则回收旧的空间。如下图所示首先对整型变量A赋值0,然后赋值B=A,再更改A的值,由于是基本数据类型,变量本身指向对保存常量的空间,更改了A只是改变了A的指向,不影响B。
在这里插入图片描述
对于复合类型这样的各种数据结构,其内部每一个元素都可以认为是一个引用,指向某一块区域,如果这个元素是基本类型的,那么同样指向一个存储了常量的内存空间,否则可以认为指向了另一个引用。用常量对复合类型进行赋值,等于建立一个引用的列表,该复合变量的每个元素都确定了指向;但使用已有的变量进行赋值,则相当于将新变量指向了原来变量的空间:
在这里插入图片描述
这时list A和B指向同一item addr 的列表,如果通过索引复制的方式修改,实际上是让某个item指向另一个空间,但是这两个list实际对应的仍是同一存储区域,修改一个,另一个也会跟着变。
在这里插入图片描述
对于复杂的数据结构类型,赋值就等于共享了资源,双方都可以修改共同的内存空间,修改对双方都有影响。

1.3 浅拷贝

如果我们需要对原始数据留存一个副本,或者不希望双方互相影响,那么赋值就不合适了,这时就需要拷贝来实现。拷贝操作是通过copy模块进行的。浅拷贝也就是一般的拷贝,使用的是copy.copy()函数。
浅拷贝的“浅”是指,无论数据结构是怎样的,都只拷贝一层:
在这里插入图片描述
如图所示,当使用B=copy.copy(A)时,列表第一层的引用列表被复制了一个副本,二者之间是互不干扰的,如果改变A中某个元素的指向,B不会受影响。这一点对于单层的数据结构可以做到完全的独立:
在这里插入图片描述
但对于多层的嵌套结构,由于只有第一层是独立的,如果修改了深层的数据,由于双方仍然指向的是同一内存空间,依然是会有影响的:
在这里插入图片描述
这里a[3]与b[3]都是列表,通过浅拷贝二者的引用互相独立,但是仍然指向的是同一个空间。

1.4 深拷贝

深拷贝的使用是通过copy模块的copy.deepcopy()函数。深拷贝是指无论多复杂的数据结构,只要元素指向的不是常量空间(元素不是基本类型)就进行复制,将引用独立开来:
在这里插入图片描述
在这里插入图片描述
深拷贝实现了完全的相互独立,无论多复杂的数据结构,都不会相互影响。

2· 简易的实现

这里我们讨论比较简单的一种情况,就是list的嵌套。被拷贝的对象要么是基本类型,要么是list或多层的list嵌套。由于对于基本类型,赋值操作符 “=” 本身即可以认为是深拷贝的,因此对于基本类型元素,直接赋值并返回即可,对于list,可以递归调用这个函数即可:

import os
import sys

def myDeepCopy(a):
    if type(a) != list:
        b=a
    else:
        b=[]
        for a_i in a:
            b.append(myDeepCopy(a_i))

    return b

if __name__ == '__main__':
    a = input()
    b = myDeepCopy(a)
    print("a= {},b= {}".format(a,b))
    a=input()
    print("a= {},b= {}".format(a,b))

这里举个简单的例子,myDeepCopy函数输入一个对象a,返回他的深拷贝结果b。如果a的类型不是list,对于当前的场景,那就是一些基本类型,直接赋值返回即可;如果是list,就对其中每个元素调用myDeepCopy方法即可。
测试如下:首先输入a,调用myDeepCopy(a)得到深拷贝结果b,打印a,b可以看到a,b是一样的;接着在输入一个新的a,再次打印,可以看到a更新了,b没有变化。
在这里插入图片描述

3· 不容易发现的问题

3.1 自定义类的赋值

例如如下的自定义类:

class testClass:
    def __init__(self,a=0,b=[1,2],c='hello'):
        print("constructor")
        self.a = a
        self.b = b
        self.c = c

if __name__ == '__main__':
    A = testClass(0,[1,2],'A')
    B = A
    C = copy.copy(A)
    D = copy.deepcopy(A)
    print("B:{},{},{}".format(B.a,B.b,B.c))
    print("C:{},{},{}".format(C.a,C.b,C.c))
    print("D:{},{},{}".format(D.a,D.b,D.c))
    A.b[0]=100
    print("B:{},{},{}".format(B.a,B.b,B.c))
    print("C:{},{},{}".format(C.a,C.b,C.c))
    print("D:{},{},{}".format(D.a,D.b,D.c))

构造对象A,并B = A,C和D分别是A的浅拷贝与深拷贝,这时ABCD的成员变量值是一样的,此时修改A的成员变量,观察发现。自定义类的赋值、深浅拷贝与复合数据类型是一致的。
在这里插入图片描述

3.2 Python快速数组扩展

Python的乘号*可以用于数组的快速扩展,例如:

a=[1,2,3]
b=a*3

那么我们会得到:
在这里插入图片描述
这时如果对b中的元素进行赋值或修改,如:

b[0]=10

则有
在这里插入图片描述
很正常的修改。
但是如果此时a数组是嵌套的list,情况就会有所不同:
在这里插入图片描述
可以看到当a数嵌套list时,通过*进行数组的扩展,得到的b也是将a重复了若干次的结果,但此时修改b中第一个数组的第一个元素,却会将每一个“循环”内的第一个数组第一个元素都修改掉。
这是因为利用乘号进行数据扩展时,被扩展部分实际上是浅拷贝的,只拷贝了第一层的数据结构。当元素是整数这样的基本类型(最底层)时,会开辟新的存储空间;而当元素是list等类型时,相当于指向了原有的空间,因此改动一个值,其他的都会跟着变化。
实际上b=a*3这样的扩展后赋值语句本身也是浅拷贝:
在这里插入图片描述
修改a之后b中对应元素的值跟着变化,因为本质都指向同一块内存空间。
这种乘法扩展数组的方法非常方便,但要注意本质是浅拷贝,避免出错。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值