Python numpy之视图与副本

288 阅读5分钟

image.png

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

前言

继上一篇对numpy 模块之ndarray一文中对 ndarray 内存结构主要分为两部分:

image.png

  • metdata :存放数组类型dtype、数组维度ndim、维度数量shape、维间距strides等
  • raw bata:存放原始数据data

metdata 中包含着关于数组相关信息,可以帮助我们在数组ndarray中快速索引和解释指定的数据

除此了对数组进行索引操作外,也会对数组的原数据进行类似与之前“拷贝”操作。

众所周知,在 Python 中大家应该对深浅拷贝有一定的印象吧,在 numpy 中则换成了“视图”与“副本”的概念了。

相信大家和我一样对此存在疑问,十万个为什么涌上心头了,“视图是什么?”,“什么是副本?”

因此,本期我们一起来学习 numpy 模块中比较新奇的概念视图与副本,Let's go~

1. 开胃菜

我们之前在学习 Python 赋值、深浅拷贝时,在代码添加中对比两个对象的地址id()是否一致。

同理,按照这样的思路,numpy 中也可以对比两个数组地址是否一样。

同时,在 numpy 数组对象ndarray 也提供许多字段,方便让我们进一步地查看数组内部的差异

  • ndarray.flags : 查看数组存储策略、读写权限、对象等

    • C_CONTIGUOUS (C) 行优先存储
    • F_CONTIGUOUS 列优先存储
    • OWNDATA 数据所有者
    • WRITEABLE 编写权限
    • ALIGNED 数据元素与硬件指针对齐
    • WRITEBACKIFCOPY 数组是其他数组的副本
    • UPDATEIFCOPY 已弃用 注:flags 相关属性名称可以单独调用例如 flags.writeable
  • ndarray.base : 查看数组中的元素是否来自其他数组

  • ndarray.nbytes: 查看数组中数据占用的字节数

  • getsizeof(item): 查看数组占用的内存空点

介绍完上述指标,我们来小试一下

>>> import numpy as np
>>> a = np.array([1,2,3,4])
>>> print(a[1:3])
[2 3]
>>> print(a[[1,2]])
[2 3]
>>>

查看a[1:3] 与 a[[1,2]] 内存地址,它们俩位置不一样,a[[1,2]]意味发生了深拷贝本(副本),a[1:3] 是原数组a引用(视图)

>>> print(id(a[1:3]))
2247482965008
>>> print(id(a[[1,2]]))
2247482964928

查看ndarray.owndata 属性,发现a[1:3] 数据来自a数组的,而a[[1,2]]是自身数据的

>>> print(a.flags.owndata)
True
>>> print(a[1:3].flags.owndata)
False
>>> print(a[[1,2]].flags.owndata)
True

我们在看一下 ndarray.base 属性,果真印证了使用 flags.owndata 查询的结果,a[1:3] 不是数据所有者,而数据来源数组a;

a[[1,2]] 是数据所有者,数据来源本身(None)

>>> print(a[[1,2]].base)
None
>>> print(a[1:3].base)
[1 2 3 4]

2. 视图

  • 视图概念

    我们通过上述简单例子,可以知道 a[1:3] 不是数据所有者,数据来源于对数组a的引用(浅拷贝)。

    因此,我们应该对视图有了基本的认识了,看一下官方怎么描述视图的

    No copy at All。 Simple assignments make no copy of objects or their data.

    视图,是对原数组进行引用拷贝,共享原始数组的数据。

  • 视图应用

    视图在numpy中广泛使用,视图一般产生有两种场景:

    • 当对原始数组进行引用时
    • 当自身无数据,与原数组共享数据时
    >>> import numpy as np
    >>> a = np.array([1,2,3,4])
    >>> b = a
    >>> b is a
    True
    >>> id(a)
    2247207679680
    >>> id(b)
    2247207679680
    >>>
    

    我们可以看到 a 与 b 是 同享同一个数据空间的

    在numpy 模块诸如索引、切片、函数view(),reshape()等返回视图结果

    >>> arr = np.arange(10)
    >>> arr_view = arr.view()
    >>> arr.shape = (2,5)
    >>> arr_reshape = arr.reshape(5,2)
    # ndarray.base 属性
    >>> print(arr.base)
    None
    >>> print(arr_view.base)
    [[0 1 2 3 4]
     [5 6 7 8 9]]
    >>> print(arr_reshape.base)
    [[0 1 2 3 4]
     [5 6 7 8 9]]
    # ndarray.flags.owndata 属性
    >>> print(arr.flags.owndata)
    True
    >>> print(arr_view.flags.owndata)
    False
    >>> print(arr_reshape.flags.owndata)
    False
    >>>
    

    image.png

  • 视图优点

    在 numpy 中 视图可以创建的对象可以节省内存空间,并且无需复制,提高查询速度

    在视图中,创建的对象如果修改数据,原始数据也被修改。

3. 副本

  • 副本概念

    副本是对原数组进行完整拷贝(数据地址也会拷贝新的),与原始数组完全独立,相对于“深拷本”,不与原始数组共享数据。

    同样截取官网,对副本的描述:

    Deep Copy The copy method makes a complete copy of the array and its data

    当改变副本的数据元素值时,虽然改变了副本与原数组相互独立,原始数组中元素值不会发生改变。

  • 副本应用

    • 当进行切片操作时
    • 当需要与原始数组数据独立时

    副本的实现我们可以直接使用 ndarray.copy()方法对原数组进行深拷贝

    b = np.array([2,5,7])
    
    c = b.copy()
    
    c[1] = 8
    
    print("b:",b)
    print("c:",c)
    
    print("c is b:",c is b)
    
    # 查看 ndarray.base 属性å
    print("b.base:",b.base)
    print("c.base:",c.base)
    
    # 查看 ndarray.flags.owndata
    print("b.flags.owndata:",b.flags.owndata)
    print("c.flags.owndata:",c.flags.owndata)
    

    image.png

    image.png

总结

本期,我们对 numpy 模块中重要的概念视图和副本。

  • 视图,相当于浅拷贝,与原数组共享数据。
  • 副本,相当于深拷贝,与原数组数据相互独立

我们可以通过内存地址id()方法,同时借助ndarray.base、ndarray.flags来进一步分析区别

以上是本期内容,欢迎大佬们点赞评论,下期见~~~