学习《Numpy基础知识》

数据类型

另见:

Data type objects

# 数组类型和类型之间的转换

Numpy支持比Python更多的数字类型。本部分显示哪些是可用的,以及如何修改数组的数据类型。

数据类型描述
bool_ 布尔(True或False),存储为一个字节
int_ 默认整数类型(与Clong相同;通常是int64int32
INTC 与Cint(通常为int32int64)相同
INTP 用于索引的整数(与Cssize_t相同;通常是int32int64
INT8 字节(-128至127)
INT16 整数(-32768至32767)
INT32 整数(-2147483648至2147483647)
Int64的 整数(-9223372036854775808至9223372036854775807)
UINT8 无符号整数(0到255)
UINT16 无符号整数(0到65535)
UINT32 无符号整数(0到4294967295)
UINT64 无符号整数(0到18446744073709551615)
float_ float64的简写。
float16 半精度浮点:符号位,5位指数,10位尾数
FLOAT32 单精度浮点数:符号位,8位指数,23位尾数
float64 双精度浮点:符号位,11位指数,52位尾数
complex_ complex128的简写。
complex64 复数,由两个32位浮点数(实部和虚部)
complex128 复数,由两个64位浮点数(实部和虚部)

除了intc之外,还定义了平台相关的C整数类型shortlonglonglong

Numpy数值类型是dtype(data-type)对象的实例,每个类型具有唯一的特征。在你使用下面的语句导入NumPy后

>>> import numpy as np

这些类型可以用np.bool_np.float32等方式访问。

未在上表中列出的高级类型,请参见结构化数组部分。

有5个基本数字类型表示布尔(bool)、整数(int)、无符号整数(uint)、浮点数(float)和复数。那些在其名称中具有数字的类型表示类型的位的大小(即,需要多少位来表示存储器中的单个值)。某些类型,例如intintp,根据平台(例如32位与64位机器)具有不同的位大小。当与存储器直接寻址的低级代码(例如C或Fortran)接口时,应该考虑这一点。

数据类型可以用作函数将python数字转换为数组标量(有关说明,请参阅数组标量部分)、将python数字序列转换为该类型的数组、或作为许多numpy函数或方法接受的dtype关键字参数。一些例子:

>>> import numpy as np
>>> x = np.float32(1.0)
>>> x
1.0
>>> y = np.int_([1,2,4])
>>> y
array([1, 2, 4])
>>> z = np.arange(3, dtype=np.uint8)
>>> z
array([0, 1, 2], dtype=uint8)

数组类型也可以由字符代码引用,主要是为了保持与旧数据包(如Numeric)的向后兼容性。有些文档可能仍然提及这些文档,例如:

>>> np.array([1, 2, 3], dtype='f')
array([ 1.,  2.,  3.], dtype=float32)

我们建议使用dtype对象。

要转换数组的类型,请使用.astype()方法(首选)或类型本身作为函数。例如:

>>> z.astype(float)                 
array([  0.,  1.,  2.])
>>> np.int8(z)
array([0, 1, 2], dtype=int8)

请注意,上面我们使用Python float对象作为dtype。NumPy 知道int指代np.int_、bool表示np.bool_、 float为np.float_以及complex为np.complex_。其他数据类型没有Python等效的类型。

要确定数组的类型,请查看dtype属性:

>>> z.dtype
dtype('uint8')

dtype对象还包含有关该类型的信息,例如其位宽和字节顺序。数据类型也可以间接用于查询类型的属性,例如是否为整数:

>>> d = np.dtype(int)
>>> d
dtype('int32')

>>> np.issubdtype(d, int)
True

>>> np.issubdtype(d, float)
False

# 数组标量

Numpy通常返回数组的元素作为数组标量(与相关dtype的标量)。数组标量与Python标量不同,但大多数情况下它们可以互换使用(主要例外是Python版本比v2.x更早的版本,其中整数数组标量不能充当列表和元组的索引)。有一些例外情况,比如代码需要非常特定的标量属性,或者当它特别检查某个值是否为Python标量时。通常,使用相应的Python类型函数(例如intfloatcomplexstrunicode)将数组标量显式转换为Python标量就很容易解决问题。

使用数组标量的主要优点是它们保留数组类型(Python可能没有可用的匹配标量类型,例如int16)。因此,使用数组标量可以确保数组和标量之间的相同行为,而不管该值是否在数组中。NumPy标量也有很多和数组相同的方法。

# 扩展精度

Python的浮点数通常是64位浮点数,几乎相当于np.float64。在某些不常见的情况下,使用Python的浮点数更精确。这在numpy中是否可行,取决于硬件和开发的环境:具体来说,x86机器提供80位精度的硬件浮点数,大多数C编译器提供它为long double类型,MSVC(Windows版本的标准)让long doubledouble(64位)完全一样。Numpy使编译器的long doublenp.longdouble(复数为np.clongdouble)。你可以用np.finfo(np.longdouble)找出你的numpy提供的是什么。

Numpy 不提供比 C 语言里 long double 更高精度的数据类型,特别是 128 位的IEEE 四倍精度的数据类型(FORTRAN的 REAL*16) 不可用。

为了有效地进行内存的校准,np.longdouble通常以零位进行填充,即96或者128位, 哪个更有效率取决于硬件和开发环境;通常在32位系统上它们被填充到96位,而在64位系统上它们通常被填充到128位。np.longdouble被填充到系统默认值;为需要特定填充的用户提供了np.float96np.float128。尽管它们的名称是这样叫的, 但是np.float96np.float128只提供与np.longdouble一样的精度, 即大多数x86机器上的80位和标准Windows版本中的64位。

请注意,即使np.longdouble提供比python float更多的精度,也很容易失去额外的精度,因为python通常强制值通过float传递值。例如,%格式操作符要求将其参数转换为标准python类型,因此即使请求了许多小数位,也不可能保留扩展精度。使用值1 + np.finfo(np.longdouble).eps测试你的代码非常有用。

数组创建

另见:

Array creation routines

# 简介

一般有5个机制创建数组:

  1. 从其他Python结构(例如,列表,元组)转换
  2. numpy原生数组的创建(例如,arange、ones、zeros等)
  3. 从磁盘读取数组,无论是标准格式还是自定义格式
  4. 通过使用字符串或缓冲区从原始字节创建数组
  5. 使用特殊库函数(例如,random)
  6. 本节不包括复制、join或以其他方式扩展或改变现有数组的方法。它也不会涉及创建对象数组或结构化数组。这两个都在它们自己的部分讲述。

本节不包括复制、join或以其他方式扩展或改变现有数组的方法。它也不会涉及创建对象数组或结构化数组。这两个都在它们自己的部分讲述。

# 将Python array_like对象转换为Numpy数组

通常,在Python中排列成array-like结构的数值数据可以通过使用array()函数转换为数组。最明显的例子是列表和元组。有关其使用的详细信息,请参阅array()的文档。一些对象可能支持数组协议并允许以这种方式转换为数组。找出对象是否可以使用array()转换为一个数组numpy 数组的简单方法很简单,只要交互式试一下,看看它是否工作!(Python方式)。

例子:

>>> x = np.array([2,3,1,0])
>>> x = np.array([2, 3, 1, 0])
>>> x = np.array([[1,2.0],[0,0],(1+1j,3.)]) # note mix of tuple and lists,
    and types
>>> x = np.array([[ 1.+0.j, 2.+0.j], [ 0.+0.j, 0.+0.j], [ 1.+1.j, 3.+0.j]])

# Numpy原生数组的创建

Numpy内置了从头开始创建数组的函数:

zeros(shape)将创建一个用指定形状用0填充的数组。默认的dtype是float64。

>>> np.zeros((2, 3)) array([[ 0., 0., 0.], [ 0., 0., 0.]])

ones(shape)将创建一个用1个值填充的数组。它在所有其他方面与zeros相同。

arange()将创建具有有规律递增值的数组。检查文档字符串以获取有关可以使用的各种方式的完整信息。这里给出几个例子:

>>> np.arange(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.arange(2, 10, dtype=np.float)
array([ 2., 3., 4., 5., 6., 7., 8., 9.])
>>> np.arange(2, 3, 0.1)
array([ 2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9])

请注意,关于用户应该注意的最后用法在arange文档字符串中有一些细微的描述。

linspace()将创建具有指定数量元素的数组,并在指定的开始值和结束值之间平均间隔。例如:

>>> np.linspace(1., 4., 6)
array([ 1. ,  1.6,  2.2,  2.8,  3.4,  4. ])

这个创建函数的优点是可以保证元素的数量以及开始和结束点,对于任意的开始,停止和步骤值,arange()通常不会这样做。

indices()将创建一组数组(堆积为一个更高维的数组),每个维度一个,每个维度表示该维度中的变化。一个例子说明比口头描述要好得多:

>>> np.indices((3,3))
array([[[0, 0, 0], [1, 1, 1], [2, 2, 2]], [[0, 1, 2], [0, 1, 2], [0, 1, 2]]])

这对于评估常规网格上多个维度的功能特别有用。

# 从磁盘读取数组

这大概是大阵列创建的最常见情况。当然,细节很大程度上取决于磁盘上的数据格式,所以本节只能给出如何处理各种格式的一般指示。

# 标准二进制格式

各种字段都有阵列数据的标准格式。下面列出了那些已知的Python库来读取它们并返回numpy数组(可能有其他可能读取并转换为numpy数组的其他数据,因此请检查最后一节)

HDF5: PyTables
FITS: PyFITS

无法直接读取但不易转换的格式示例是像PIL这样的库支持的格式(能够读取和写入许多图像格式,如jpg,png等)。

# 常见ASCII格式

逗号分隔值文件(CSV)被广泛使用(以及Excel等程序的导出和导入选项)。有很多方法可以在Python中阅读这些文件。python中有CSV函数和pylab函数(matplotlib的一部分)。

更多通用的ascii文件可以在scipy中使用io软件包读取。

# 自定义二进制格式

有各种各样的方法可以使用。如果文件具有相对简单的格式,那么可以编写一个简单的I / O库,并使用numpy fromfile()函数和.tofile()方法直接读取和写入numpy数组(尽管介意您的字节序)!如果存在一个读取数据的良好C或C ++库,可以使用各种技术来封装该库,但这肯定要做得更多,并且需要更多的高级知识才能与C或C ++接口。

# 使用特殊库

有些库可用于生成特殊用途的数组,并且不可能列举所有这些数组。最常见的用途是随机使用许多数组生成函数,这些函数可以生成随机值数组,以及一些用于生成特殊矩阵(例如对角线)的效用函数。

NumPy I/O操作

# 使用genfromtxt导入数据

NumPy提供了几个函数来根据表格数据创建数组。我们将重点放在genfromtxt函数上。

In a nutshell, genfromtxt runs two main loops. 第一个循环以字符串序列转换文件的每一行。第二个循环将每个字符串转换为适当的数据类型。这种机制比单一循环慢,但提供了更多的灵活性。特别的, genfromtxt考虑到缺失值的情况, 其他更简单的方法如loadtxt无法做到这点.

注意
举例时,我们将使用以下约定:

>>> import numpy as np
>>> from io import BytesIO

# 定义输入

genfromtxt的唯一强制参数是数据的来源。它可以是一个字符串,一串字符串或一个生成器。如果提供了单个字符串,则假定它是本地或远程文件的名称,或者带有read方法的开放文件类对象,例如文件或StringIO.StringIO对象。如果提供了字符串列表或生成器返回字符串,则每个字符串在文件中被视为一行。当传递远程文件的URL时,该文件将自动下载到当前目录并打开。

识别的文件类型是文本文件和档案。目前,该功能可识别gzipbz2(bzip2)档案。归档文件的类型由文件的扩展名决定:如果文件名以'.gz'结尾,则需要一个gzip归档文件;如果它以'bz2'结尾,则假定bzip2存档。


# 将行拆分为列

# delimiter参数

一旦文件被定义并打开进行读取,genfromtxt会将每个非空行分割为一串字符串。 空的或注释的行只是略过。 delimiter关键字用于定义拆分应该如何进行。

通常,单个字符标记列之间的分隔。例如,逗号分隔文件(CSV)使用逗号(,)或分号(;)作为分隔符:

>>> data = "1, 2, 3
4, 5, 6"
>>> np.genfromtxt(BytesIO(data), delimiter=",")
array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.]])

另一个常用的分隔符是" ",即制表符。但是,我们不限于单个字符,任何字符串都可以。默认情况下,genfromtxt假定delimiter=None,这意味着该行沿着空白区域(包括制表符)分割,并且连续的空白区域被视为单个空白区域。

或者,我们可能正在处理一个固定宽度的文件,其中列被定义为给定数量的字符。在这种情况下,我们需要将delimiter设置为单个整数(如果所有列的大小相同)或整数序列(如果列的大小可能不同):

>>> data = "  1  2  3
  4  5 67
890123  4"
>>> np.genfromtxt(BytesIO(data), delimiter=3)
array([[   1.,    2.,    3.],
       [   4.,    5.,   67.],
       [ 890.,  123.,    4.]])
>>> data = "123456789
   4  7 9
   4567 9"
>>> np.genfromtxt(BytesIO(data), delimiter=(4, 3, 2))
array([[ 1234.,   567.,    89.],
       [    4.,     7.,     9.],
       [    4.,   567.,     9.]])

# autostrip参数

默认情况下,当一行被分解为一系列字符串时,单个条目不会被剥离前导空白或尾随空白。通过将可选参数autostrip设置为值True,可以覆盖此行为:

>>> data = "1, abc , 2
 3, xxx, 4"
>>> # Without autostrip
>>> np.genfromtxt(BytesIO(data), delimiter=",", dtype="|S5")
array([['1', ' abc ', ' 2'],
       ['3', ' xxx', ' 4']],
      dtype='|S5')
>>> # With autostrip
>>> np.genfromtxt(BytesIO(data), delimiter=",", dtype="|S5", autostrip=True)
array([['1', 'abc', '2'],
       ['3', 'xxx', '4']],
      dtype='|S5')

# comments参数

可选参数comments用于定义标记注释开始的字符串。默认情况下,genfromtxt假定comments='#'。评论标记可能发生在线上的任何地方。评论标记之后的任何字符都会被忽略:

>>> data = """#
... # Skip me !
... # Skip me too !
... 1, 2
... 3, 4
... 5, 6 #This is the third line of the data
... 7, 8
... # And here comes the last line
... 9, 0
... """
>>> np.genfromtxt(BytesIO(data), comments="#", delimiter=",")
[[ 1.  2.]
 [ 3.  4.]
 [ 5.  6.]
 [ 7.  8.]
 [ 9.  0.]]

注意!
这种行为有一个明显的例外:如果可选参数names=True,则会检查第一条注释行的名称。


# 跳过直线并选择列

# skip_headerskip_footer参数

文件中存在标题可能会妨碍数据处理。在这种情况下,我们需要使用skip_header可选参数。此参数的值必须是一个整数,与执行任何其他操作之前在文件开头跳过的行数相对应。同样,我们可以使用skip_footer属性跳过文件的最后一行n,并给它一个n的值:

>>> data = "
".join(str(i) for i in range(10))
>>> np.genfromtxt(BytesIO(data),)
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.])
>>> np.genfromtxt(BytesIO(data),
...               skip_header=3, skip_footer=5)
array([ 3.,  4.])

默认情况下,skip_header=0skip_footer=0,这意味着不会跳过任何行。

# usecols参数

在某些情况下,我们对数据的所有列不感兴趣,但只有其中的一小部分。我们可以用usecols参数选择要导入的列。该参数接受与要导入的列的索引相对应的单个整数或整数序列。请记住,按照惯例,第一列的索引为0。负整数的行为与常规Python负向索引相同。

例如,如果我们只想导入第一列和最后一列,我们可以使用usecols =(0, -1)

>>> data = "1 2 3
4 5 6"
>>> np.genfromtxt(BytesIO(data), usecols=(0, -1))
array([[ 1.,  3.],
       [ 4.,  6.]])

如果列有名称,我们也可以通过将它们的名称提供给usecols参数来选择要导入哪些列,可以将其作为字符串序列或逗号分隔字符串:

>>> data = "1 2 3
4 5 6"
>>> np.genfromtxt(BytesIO(data),
...               names="a, b, c", usecols=("a", "c"))
array([(1.0, 3.0), (4.0, 6.0)],
      dtype=[('a', '<f8'), ('c', '<f8')])
>>> np.genfromtxt(BytesIO(data),
...               names="a, b, c", usecols=("a, c"))
    array([(1.0, 3.0), (4.0, 6.0)],
          dtype=[('a', '<f8'), ('c', '<f8')])

# 选择数据的类型

控制我们从文件中读取的字符串序列如何转换为其他类型的主要方法是设置dtype参数。这个参数的可接受值是:

  • 单一类型,如dtype=float。除非使用names参数将名称与每个列关联(见下文),否则输出将是给定dtype的2D格式。请注意,dtype=floatgenfromtxt的默认值。
  • 一系列类型,如dtype =(int, float, float)
  • 逗号分隔的字符串,例如dtype="i4,f8,|S3"
  • 一个包含两个键'names''formats'的字典。
  • a sequence of tuples(name, type), such as dtype=[('A', int), ('B', float)].
  • 现有的numpy.dtype对象。
  • 特殊值None。在这种情况下,列的类型将根据数据本身确定(见下文)。

在所有情况下,除了第一种情况,输出将是一个带有结构化dtype的一维数组。这个dtype与序列中的项目一样多。字段名称由names关键字定义。

dtype=None时,每列的类型由其数据迭代确定。我们首先检查一个字符串是否可以转换为布尔值(也就是说,如果字符串在小写字母中匹配truefalse);然后是否可以将其转换为整数,然后转换为浮点数,然后转换为复数并最终转换为字符串。通过修改StringConverter类的默认映射器可以更改此行为。

为方便起见,提供了dtype=None选项。但是,它明显比显式设置dtype要慢。


# 设置名称

# names参数

处理表格数据时的一种自然方法是为每列分配一个名称。如前所述,第一种可能性是使用明确的结构化dtype。

>>> data = BytesIO("1 2 3
 4 5 6")
>>> np.genfromtxt(data, dtype=[(_, int) for _ in "abc"])
array([(1, 2, 3), (4, 5, 6)],
      dtype=[('a', '<i8'), ('b', '<i8'), ('c', '<i8')])

另一种更简单的可能性是将names关键字与一系列字符串或逗号分隔的字符串一起使用:

>>> data = BytesIO("1 2 3
 4 5 6")
>>> np.genfromtxt(data, names="A, B, C")
array([(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)],
      dtype=[('A', '<f8'), ('B', '<f8'), ('C', '<f8')])

在上面的例子中,我们使用了默认情况下dtype=float的事实。通过给出一个名称序列,我们强制输出到一个结构化的dtype。

我们有时可能需要从数据本身定义列名。在这种情况下,我们必须使用names关键字的值为True。这些名字将从第一行(在skip_header之后)被读取,即使该行被注释掉:

>>> data = BytesIO("So it goes
#a b c
1 2 3
 4 5 6")
>>> np.genfromtxt(data, skip_header=1, names=True)
array([(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)],
      dtype=[('a', '<f8'), ('b', '<f8'), ('c', '<f8')])

names的默认值为None。如果我们给关键字赋予任何其他值,新名称将覆盖我们可能用dtype定义的字段名称:

>>> data = BytesIO("1 2 3
 4 5 6")
>>> ndtype=[('a',int), ('b', float), ('c', int)]
>>> names = ["A", "B", "C"]
>>> np.genfromtxt(data, names=names, dtype=ndtype)
array([(1, 2.0, 3), (4, 5.0, 6)],
      dtype=[('A', '<i8'), ('B', '<f8'), ('C', '<i8')])

# defaultfmt参数

如果 names=None 的时候,只是预计会有一个结构化的dtype,它的名称将使用标准的NumPy默认值 "f%i"来定义,会产生例如f0f1等名称:

>>> data = BytesIO("1 2 3
 4 5 6")
>>> np.genfromtxt(data, dtype=(int, float, int))
array([(1, 2.0, 3), (4, 5.0, 6)],
      dtype=[('f0', '<i8'), ('f1', '<f8'), ('f2', '<i8')])

同样,如果我们没有提供足够的名称来匹配dtype的长度,缺少的名称将使用此默认模板进行定义:

>>> data = BytesIO("1 2 3
 4 5 6")
>>> np.genfromtxt(data, dtype=(int, float, int), names="a")
array([(1, 2.0, 3), (4, 5.0, 6)],
      dtype=[('a', '<i8'), ('f0', '<f8'), ('f1', '<i8')])

我们可以使用defaultfmt参数覆盖此默认值,该参数采用任何格式字符串:

>>> data = BytesIO("1 2 3
 4 5 6")
>>> np.genfromtxt(data, dtype=(int, float, int), defaultfmt="var_%02i")
array([(1, 2.0, 3), (4, 5.0, 6)],
      dtype=[('var_00', '<i8'), ('var_01', '<f8'), ('var_02', '<i8')])

注意!
我们需要记住,仅当预期一些名称但未定义时才使用defaultfmt

# 验证名称

具有结构化dtype的NumPy数组也可以被视为recarray,其中可以像访问属性一样访问字段。因此,我们可能需要确保字段名称不包含任何空格或无效字符,或者它不对应于标准属性的名称(如sizeshape),这会混淆解释者。genfromtxt接受三个可选参数,这些参数可以更好地控制名称:

  • deletechars
    给出一个字符串,将所有必须从名称中删除的字符组合在一起。默认情况下,无效字符是~!@#$%^&*()-=+~|]}[{';: /?.>,<
  • excludelist
    给出要排除的名称列表,如returnfileprint ...如果其中一个输入名称是该列表的一部分,则会附加一个下划线字符('_')。
  • case_sensitive
    是否区分大小写(case_sensitive=True),转换为大写(case_sensitive=Falsecase_sensitive='upper')或小写(case_sensitive='lower')。

# 调整转换

# converters参数

通常,定义一个dtype足以定义字符串序列必须如何转换。但是,有时可能需要一些额外的控制。例如,我们可能希望确保格式为YYYY/MM/DD的日期转换为datetime对象,或者像xx%正确转换为0到1之间的浮点数。在这种情况下,我们应该使用converters参数定义转换函数。

该参数的值通常是以列索引或列名称作为关键字的字典,并且转换函数作为值。这些转换函数可以是实际函数或lambda函数。无论如何,它们只应接受一个字符串作为输入,并只输出所需类型的单个元素。

在以下示例中,第二列从代表百分比的字符串转换为0和1之间的浮点数:

>>> convertfunc = lambda x: float(x.strip("%"))/100.
>>> data = "1, 2.3%, 45.
6, 78.9%, 0"
>>> names = ("i", "p", "n")
>>> # General case .....
>>> np.genfromtxt(BytesIO(data), delimiter=",", names=names)
array([(1.0, nan, 45.0), (6.0, nan, 0.0)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

我们需要记住,默认情况下,dtype=float。因此,对于第二列期望浮点数。但是,字符串'2.3%''78.9%无法转换为浮点数,我们最终改为使用np.nan。现在让我们使用一个转换器:

>>> # Converted case ...
>>> np.genfromtxt(BytesIO(data), delimiter=",", names=names,
...               converters={1: convertfunc})
array([(1.0, 0.023, 45.0), (6.0, 0.78900000000000003, 0.0)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

通过使用第二列("p")作为关键字而不是其索引(1)的名称,可以获得相同的结果:

>>> # Using a name for the converter ...
>>> np.genfromtxt(BytesIO(data), delimiter=",", names=names,
...               converters={"p": convertfunc})
array([(1.0, 0.023, 45.0), (6.0, 0.78900000000000003, 0.0)],
      dtype=[('i', '<f8'), ('p', '<f8'), ('n', '<f8')])

转换器也可以用来为缺少的条目提供默认值。在以下示例中,如果字符串为空,则转换器convert会将已剥离的字符串转换为相应的浮点型或转换为-999。我们需要明确地从空白处去除字符串,因为它并未默认完成:

>>> data = "1, , 3
 4, 5, 6"
>>> convert = lambda x: float(x.strip() or -999)
>>> np.genfromtxt(BytesIO(data), delimiter=",",
...               converters={1: convert})
array([[   1., -999.,    3.],
       [   4.,    5.,    6.]])

# 使用缺失值和填充值

我们尝试导入的数据集中可能缺少一些条目。在前面的例子中,我们使用转换器将空字符串转换为浮点。但是,用户定义的转换器可能会很快变得繁琐,难以管理。

genfromtxt函数提供了另外两种补充机制:missing_values参数用于识别丢失的数据,第二个参数filling_values用于处理这些缺失的数据。

# missing_values

默认情况下,任何空字符串都被标记为缺失。我们也可以考虑更复杂的字符串,比如"N/A""???"代表丢失或无效的数据。missing_values参数接受三种值:

  • 一个字符串或逗号分隔的字符串
    该字符串将用作所有列缺失数据的标记
  • 一串字符串
    在这种情况下,每个项目都按顺序与列关联。
  • 一本字典
    字典的值是字符串或字符串序列。相应的键可以是列索引(整数)或列名称(字符串)。另外,可以使用特殊键None来定义适用于所有列的默认值。

# filling_values

我们知道如何识别丢失的数据,但我们仍然需要为这些丢失的条目提供一个值。默认情况下,根据此表根据预期的dtype确定此值:

我们知道如何识别丢失的数据,但我们仍然需要为这些丢失的条目提供一个值。默认情况下,根据此表根据预期的dtype确定此值:

预期类型默认
bool False
int -1
float np.nan
complex np.nan+0j
string '???'

通过filling_values可选参数,我们可以更好地控制缺失值的转换。像missing_values一样,此参数接受不同类型的值:

  • 一个单一的价值
    这将是所有列的默认值
  • 一系列值
    每个条目都是相应列的默认值
  • 一本字典
    每个键可以是列索引或列名称,并且相应的值应该是单个对象。我们可以使用特殊键None为所有列定义默认值。

在下面的例子中,我们假设缺少的值在第一列中用"N/A"标记,并由"???"在第三栏。如果它们出现在第一列和第二列中,我们希望将这些缺失值转换为0,如果它们出现在最后一列中,则将它们转换为-999:

>>> data = "N/A, 2, 3
4, ,???"
>>> kwargs = dict(delimiter=",",
...               dtype=int,
...               names="a,b,c",
...               missing_values={0:"N/A", 'b':" ", 2:"???"},
...               filling_values={0:0, 'b':0, 2:-999})
>>> np.genfromtxt(BytesIO(data), **kwargs)
array([(0, 2, 3), (4, 0, -999)],
      dtype=[('a', '<i8'), ('b', '<i8'), ('c', '<i8')])

# usemask

我们也可能想通过构造一个布尔掩码来跟踪丢失数据的发生,其中True条目缺少数据,否则False。为此,我们只需将可选参数usemask设置为True(默认值为False)。输出数组将成为MaskedArray


# 快捷方式功能

In addition to genfromtxt, the numpy.lib.io module provides several convenience functions derived from genfromtxt. 这些函数与原始函数的工作方式相同,但它们具有不同的默认值。

  • ndfromtxt:始终设置usemask=False。输出总是一个标准的numpy.ndarray
  • mafromtxt:始终设置usemask=True。输出总是一个MaskedArray
  • recfromtxt:返回一个标准的numpy.recarray(如果usemask=False)或一个MaskedRecords数组(如果usemaske=True) 。默认的dtype是dtype=None,这意味着每列的类型将被自动确定。
  • recfromcsv:类似于recfromtxt,但使用默认的delimiter=","

索引

另见:

Indexing routines

数组索引指的是使用方括号([])来索引数组值。有很多选项来索引,这使numpy索引很强大,但功能上的强大也带来一些复杂性和潜在的混乱。本部分只是与索引相关的各种选项和问题的概述。除了单个元素索引之外,大多数这些选项的细节可以在相关章节中找到。

# 赋值与引用

以下大多数示例显示了在引用数组中的数据时使用索引。这些示例在给数组赋值时同样适用。有关如何赋值的具体示例和说明,请参阅末尾的部分。

# 单个元素索引

一维数组的单元素索引是人们所期望的。它的工作方式与其他标准Python序列完全相同。它是从0开始的,并且接受负索引来从数组的结尾进行索引。

>>> x = np.arange(10)
>>> x[2]
2
>>> x[-2]
8

与列表和元组不同,numpy数组支持多维数组的多维索引。这意味着没有必要将每个维度的索引分成它自己的一组方括号。

>>> x.shape = (2,5) # now x is 2-dimensional
>>> x[1,3]
8
>>> x[1,-1]
9

请注意,如果索引索引数量少于维度的多维数组,则会得到一个子维数组。例如:

>>> x[0]
array([0, 1, 2, 3, 4])

也就是说,指定的每个索引都会选择与所选维度的其余部分相对应的数组。在上面的例子中,选择0表示长度为5的剩余维度未指定,并且返回的是具有该维度和大小的数组。必须注意的是,返回的数组不是原始数据的副本,而是指向与原始数组相同的内存值。在这种情况下,返回第一个位置(0)处的一维数组。因此,在返回的数组上使用单个索引会导致返回一个元素。那是:

>>> x[0][2]
2

因此,请注意,x [0,2] = x [0] [2], 但是第二种情况效率更低,因为一个新的临时数组在第一个索引后创建了,这个临时数组随后才被2这个数字索引。

请注意那些用于IDL或Fortran内存顺序的索引。Numpy使用C顺序索引。这意味着最后一个索引通常代表了最快速变化的内存位置,与Fortran或IDL不同,第一个索引代表内存中变化最快的位置。这种差异代表着很大的混乱的可能性。

# 其他索引选项

可以对数组进行切片和步进,以提取具有相同数量维数的数组,但其大小与原始数据不同。切片和跨步的工作方式与对列表和元组完全相同,除此之外它们还可以应用于多个维度。几个例子说明了最好的:

>>> x = np.arange(10)
>>> x[2:5]
array([2, 3, 4])
>>> x[:-7]
array([0, 1, 2])
>>> x[1:7:2]
array([1, 3, 5])
>>> y = np.arange(35).reshape(5,7)
>>> y[1:5:2,::3]
array([[ 7, 10, 13],
       [21, 24, 27]])

请注意,数组切片不会复制内部数组数据,但也会产生原始数据的新视图。

可以用其他数组来索引数组,以便从数组中选择相应列表的值来产生新的数组。有两种不同的方式来完成这一点。一个使用一个或多个索引值数组。另一个涉及给出一个适当形状的布尔数组来指示要选择的值。索引数组是一个非常强大的工具,可以避免在数组中循环各个元素,从而大大提高性能。

可以使用特殊功能通过索引有效增加数组中的维数,以便生成的数组获得在表达式或特定函数中使用所需的形状。

# 索引数组

Numpy数组可以被其他数组(或任何其他可转换为数组的类似序列的对象,例如除了元组之外的列表)索引;有关为什么会出现这种情况,请参阅本文档的末尾。索引数组的使用范围从简单,直接的情况到复杂的难以理解的情况。对于索引数组的所有情况,返回的是原始数据的副本,而不是片段获取的视图。

索引数组必须是整数类型。数组中的每个值指示数组中要使用哪个值来代替索引。为了显示:

>>> x = np.arange(10,1,-1)
>>> x
array([10,  9,  8,  7,  6,  5,  4,  3,  2])
>>> x[np.array([3, 3, 1, 8])]
array([7, 7, 9, 2])

由值3,3,1和8组成的索引数组相应地创建了一个长度为4的数组(与索引数组相同),其中每个索引被索引数组在索引中具有的值替换。

负值是允许的,并且与单个索引或片一样工作。

>>> x[np.array([3,3,-3,8])]
array([7, 7, 4, 2])

索引值超出范围是错误的:

>>> x[np.array([3, 3, 20, 8])]
<type 'exceptions.IndexError'>: index 20 out of bounds 0<=index<9

一般来说,使用索引数组时返回的是与索引数组具有相同形状的数组,但是索引数组的类型和值。作为一个例子,我们可以使用多维索引数组来代替:

>>> x[np.array([[1,1],[2,3]])]
array([[9, 9],
       [8, 7]])

# 索引多维数组

对多维数组进行索引时,情况会变得更加复杂,特别是对于多维索引数组。这些往往是更常用的用途,但它们是允许的,并且它们对于一些问题是有用的。我们将从最简单的多维情况开始(使用前面例子中的数组y):

>>> y[np.array([0,2,4]), np.array([0,1,2])]
array([ 0, 15, 30])

在这种情况下,如果索引数组具有匹配的形状,并且索引数组的每个维都有一个索引数组,则结果数组具有与索引数组相同的形状,并且这些值对应于每个索引集的索引在索引数组中的位置。在此示例中,两个索引数组的第一个索引值为0,因此结果数组的第一个值为y [0,0]。下一个值是y [2,1],最后一个是y [4,2]。

如果索引数组不具有相同的形状,则会尝试将它们广播到相同的形状。如果它们不能以相同的形状广播,则会引发异常:

>>> y[np.array([0,2,4]), np.array([0,1])]
<type 'exceptions.ValueError'>: shape mismatch: objects cannot be

broadcast to a single shape
广播机制允许索引数组与其他索引的标量组合。结果是标量值用于索引数组的所有对应值:

>>> y[np.array([0,2,4]), 1]
array([ 1, 15, 29])

跳到复杂性的下一个级别,可以只用索引数组部分索引数组。理解在这种情况下会发生什么需要一些思考。例如,如果我们只使用一个索引数组与y:

>>> y[np.array([0,2,4])]
array([[ 0,  1,  2,  3,  4,  5,  6],
       [14, 15, 16, 17, 18, 19, 20],
       [28, 29, 30, 31, 32, 33, 34]])

什么结果是一个新的数组的结构,其中索引数组的每个值从被索引的数组中选择一行,并且结果数组具有结果形状(行的大小,数字索引元素)。

这可能有用的一个示例是用于颜色查找表,我们想要将图像的值映射到RGB三元组中进行显示。查找表可能有一个形状(nlookup,3)。使用带有dtype = np.uint8(或任何整数类型,只要值与查找表的边界)形状(ny,nx)的图像索引这样一个数组将导致一个形状数组(ny,nx, 3)RGB值的三倍与每个像素位置相关联。

通常,resulant数组的形状将是索引数组形状(或所有索引数组广播的形状)与索引数组中任何未使用的维(未索引的维)的形状的串联。

# 布尔值或掩码索引数组

用作索引的布尔数组的处理方式完全不同于索引数组。布尔数组的形状必须与被索引数组的初始维相同。在最直接的情况下,布尔数组具有相同的形状:

>>> b = y>20
>>> y[b]
array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34])

与整数索引数组不同,在布尔情况下,结果是一维数组,其中包含索引数组中所有对应于布尔数组中所有真实元素的元素。索引数组中的元素始终以行优先(C样式)顺序进行迭代和返回。结果也与y[np.nonzero(b)]相同。与索引数组一样,返回的是数据的副本,而不是像切片一样获得的视图。

如果y比b的维数更高,则结果将是多维的。例如:

>>> b[:,5] # use a 1-D boolean whose first dim agrees with the first dim of y
array([False, False, False,  True,  True], dtype=bool)
>>> y[b[:,5]]
array([[21, 22, 23, 24, 25, 26, 27],
       [28, 29, 30, 31, 32, 33, 34]])

这里第4行和第5行是从索引数组中选择出来的,并组合起来构成一个二维数组。

一般来说,当布尔数组的维数小于被索引的数组时,这相当于y [b,...],这意味着y由b索引,后面跟着很多:如填充年。因此,结果的形状是一维,其中包含布尔数组的True元素的数量,然后是索引的数组的其余维度。

例如,使用具有四个真实元素的形状(2,3)的二维布尔阵列来从三维形状(2,3,5)阵列中选择行会得到形状的二维结果(4 ,5):

>>> x = np.arange(30).reshape(2,3,5)
>>> x
array([[[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14]],
       [[15, 16, 17, 18, 19],
        [20, 21, 22, 23, 24],
        [25, 26, 27, 28, 29]]])
>>> b = np.array([[True, True, False], [False, True, True]])
>>> x[b]
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [20, 21, 22, 23, 24],
       [25, 26, 27, 28, 29]])

有关更多详细信息,请参阅有关数组索引的numpy参考文档。

# 组合索引和切片

索引数组可以与切片组合。例如:

>>> y[np.array([0,2,4]),1:3]
array([[ 1,  2],
       [15, 16],
       [29, 30]])

实际上,切片被转换为索引数组np.array([[1,2]])(形状(1,2)),该数组与索引数组一起广播以产生形状的结果数组(3,2) 。

同样,切片可以与广播布尔指数结合使用:

>>> y[b[:,5],1:3]
array([[22, 23],
       [29, 30]])

# 结构化索引工具

为了便于将数组形状与表达式和赋值相匹配,可以在数组索引中使用np.newaxis对象来添加大小为1的新维度。例如:

>>> y.shape
(5, 7)
>>> y[:,np.newaxis,:].shape
(5, 1, 7)

请注意,数组中没有新元素,只是增加了维度。这可以很方便地以一种其他方式需要明确重塑操作的方式组合两个数组。例如:

>>> x = np.arange(5)
>>> x[:,np.newaxis] + x[np.newaxis,:]
array([[0, 1, 2, 3, 4],
       [1, 2, 3, 4, 5],
       [2, 3, 4, 5, 6],
       [3, 4, 5, 6, 7],
       [4, 5, 6, 7, 8]])

省略号语法可以用于指示完整地选择任何剩余的未指定的维度。例如:

>>> z = np.arange(81).reshape(3,3,3,3)
>>> z[1,...,2]
array([[29, 32, 35],
       [38, 41, 44],
       [47, 50, 53]])

这相当于:

>>> z[1,:,:,2]
array([[29, 32, 35],
       [38, 41, 44],
       [47, 50, 53]])

# 给被索引的数组赋值

如前所述,可以使用单个索引,切片以及索引和掩码数组来选择数组的子集。分配给索引数组的值必须是形状一致的(形状与索引产生的形状相同或相同)。例如,允许为片分配一个常量:

>>> x = np.arange(10)
>>> x[2:7] = 1

或者正确大小的数组:

>>> x[2:7] = np.arange(5)

请注意,如果将较高类型分配给较低类型(在int类型中添加浮点数(floats))或甚至导致异常(将复数分配给int/float类型),分配可能会导致更改:

>>> x[1] = 1.2
>>> x[1]
1
>>> x[1] = 1.2j
<type 'exceptions.TypeError'>: can't convert complex to long; use
long(abs(z))

与一些引用(如数组和掩码索引)不同,赋值通常是对数组中的原始数据进行赋值的(事实上,没有其他意义了!)。但请注意,有些行为可能不会如人们所期望的那样行事。这个特殊的例子通常令人惊讶:

>>> x = np.arange(0, 50, 10)
>>> x
array([ 0, 10, 20, 30, 40])
>>> x[np.array([1, 1, 3, 1])] += 1
>>> x
array([ 0, 11, 20, 31, 40])

人们预计第一个地点会增加3。实际上,它只会增加1。原因是因为从原始数据(作为临时数据)中提取了一个新的数组,其中包含1,1,1,1,1,1的值,则将值1添加到临时数据中,然后将临时数据分配回原始数组。因此,x [1] +1处的数组值被分配给x [1]三次,而不是递增3次。

# 处理程序中可变数量的索引

索引语法非常强大,但在处理可变数量的索引时受到限制。例如,如果你想编写一个可以处理各种维数参数的函数,而不必为每个可能维数编写特殊的代码,那又怎么办?如果向元组提供元组,则该元组将被解释为索引列表。例如(使用数组z的前一个定义):

>>> indices = (1,1,1,1)
>>> z[indices]
40

所以可以使用代码来构造任意数量的索引的元组,然后在索引中使用这些元组。

切片可以通过在Python中使用slice()函数在程序中指定。例如:

>>> indices = (1,1,1,slice(0,2)) # same as [1,1,1,0:2]
>>> z[indices]
array([39, 40])

同样,省略号可以通过使用省略号对象的代码指定:

>>> indices = (1, Ellipsis, 1) # same as [1,...,1]
>>> z[indices]
array([[28, 31, 34],
       [37, 40, 43],
       [46, 49, 52]])

由于这个原因,可以直接使用np.where()函数的输出作为索引,因为它总是返回索引数组的元组。

由于元组的特殊处理,它们不会自动转换为列表。举个例子:

>>> z[[1,1,1,1]] # produces a large array
array([[[[27, 28, 29],
         [30, 31, 32], ...
>>> z[(1,1,1,1)] # returns a single value
40

广播(Broadcasting)

另见:

numpy.broadcast

术语broadcasting描述numpy在算术运算期间如何处理具有不同形状的数组。受限于某些约束,较小的数组依据较大数组“broadcasting”,使得它们具有兼容的形状。Broadcasting提供了一种矢量化数组操作的方法,使得循环发生在C而不是Python。它做到这一点且不用不必要的数据拷贝,通常导致高效的算法实现。然而,有些情况下,broadcasting是一个坏主意,因为它导致低效的内存使用并减慢计算。

NumPy操作通常是在逐个元素的基础上在数组对上完成的。在最简单的情况下,两个数组必须具有完全相同的形状,如下例所示:

>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([ 2.,  4.,  6.])

当数组的形状满足一定的条件时,NumPy的broadcasting规则可以放宽这个限制。最简单的broadcasting示例发生在一个操作包含数组和标量值的时候:

>>> a = np.array([1.0, 2.0, 3.0])
>>> b = 2.0
>>> a * b
array([ 2.,  4.,  6.])

结果等同于前面的例子,其中b是一个数组。在算术运算期间,我们可以认为标量b被拉伸了,形成与a相同形状的数组。b中的新元素是原始标量简单的拷贝。拉伸这个比喻只是概念性的。NumPy足够聪明,它使用原始的标量值而不会真正拷贝,使broadcasting操作尽可能的内存和计算高效。

第二个例子中的代码比第一个例子中的代码更有效,因为broadcasting在乘法期间移动较少的内存(b是标量而不是数组)。

# Broadcasting的一般规则

当在两个数组上操作时,NumPy在元素级别比较它们的形状。它从尾随的维度开始,并朝着前进的方向前进。两个维度兼容,当

  1. 他们是平等的,或者
  2. 其中之一是1

如果不满足这些条件,则抛出ValueError: frames are not aligned异常,指示数组具有不兼容的形状。结果数组的大小是沿着输入数组的每个维度的最大大小。

数组不需要具有相同维度的数目。例如,如果你有一个256x256x3数值的RGB值,并且你想要通过一个不同的值缩放图像中的每个颜色,你可以将图像乘以一个具有3个值的一维数组。根据broadcast规则排列这些数组的最后一个轴的大小,表明它们是兼容的:

Image  (3d array): 256 x 256 x 3
Scale  (1d array):             3
Result (3d array): 256 x 256 x 3

当比较的任何一个维度为1时,则使用另一个。换句话说,大小为1的维被拉伸或“复制”以匹配另一维。

在以下示例中,A和B数组都具有长度为1的轴,在broadcast操作期间将其扩展为更大的大小:

A      (4d array):  8 x 1 x 6 x 1
B      (3d array):      7 x 1 x 5
Result (4d array):  8 x 7 x 6 x 5

这里有一些例子:

A      (2d array):  5 x 4
B      (1d array):      1
Result (2d array):  5 x 4

A      (2d array):  5 x 4
B      (1d array):      4
Result (2d array):  5 x 4

A      (3d array):  15 x 3 x 5
B      (3d array):  15 x 1 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 1
Result (3d array):  15 x 3 x 5

以下是不broadcast的形状示例:

A      (1d array):  3
B      (1d array):  4 # trailing dimensions do not match

A      (2d array):      2 x 1
B      (3d array):  8 x 4 x 3 # second from last dimensions mismatched

broadcasting在实践中的一个例子:

>>> x = np.arange(4)
>>> xx = x.reshape(4,1)
>>> y = np.ones(5)
>>> z = np.ones((3,4))

>>> x.shape
(4,)

>>> y.shape
(5,)

>>> x + y
<type 'exceptions.ValueError'>: shape mismatch: objects cannot be broadcast to a single shape

>>> xx.shape
(4, 1)

>>> y.shape
(5,)

>>> (xx + y).shape
(4, 5)

>>> xx + y
array([[ 1.,  1.,  1.,  1.,  1.],
       [ 2.,  2.,  2.,  2.,  2.],
       [ 3.,  3.,  3.,  3.,  3.],
       [ 4.,  4.,  4.,  4.,  4.]])

>>> x.shape
(4,)

>>> z.shape
(3, 4)

>>> (x + z).shape
(3, 4)

>>> x + z
array([[ 1.,  2.,  3.,  4.],
       [ 1.,  2.,  3.,  4.],
       [ 1.,  2.,  3.,  4.]])

Broadcasting提供了获取两个数组的外积(或任何其他outer操作)的方便方式。以下示例显示了两个1-d数组的外积操作:

>>> a = np.array([0.0, 10.0, 20.0, 30.0])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a[:, np.newaxis] + b
array([[  1.,   2.,   3.],
       [ 11.,  12.,  13.],
       [ 21.,  22.,  23.],
       [ 31.,  32.,  33.]])

这里newaxis索引操作符将一个新轴插入到a中,使其成为一个二维4x1数组。将4x1数组与形状为(3,)b组合,产生一个4x3数组。

有关broadcasting概念的图解,请参阅本文

字节交换

# 字节排序和ndarrays简介

ndarray 是为内存中的数据提供python数组接口的对象。

经常发生的情况是,您想要使用数组查看的内存与您运行Python的计算机的字节顺序不同。

例如,我可能正在使用小端CPU的计算机(例如Intel Pentium),但是我已经从大端的计算机写入的文件中加载了一些数据。假设我已经从Sun(big-endian)计算机写入的文件中加载了4个字节。我知道这4个字节代表两个16位整数。在big-endian机器上,最高有效字节(MSB)首先存储一个双字节整数,然后存储最低有效字节(LSB)。因此,这些字节按内存顺序排列:

  1. MSB整数1
  2. LSB整数1
  3. MSB整数2
  4. LSB整数2

假设这两个整数实际上是1和770。由于770 = 256 * 3 + 2,内存中的4个字节将分别包含:0,1,3,2。我从文件中加载的字节将包含以下内容:

>>> big_end_str = chr(0) + chr(1) + chr(3) + chr(2)
>>> big_end_str
'x00x01x03x02'

我们可能想要使用ndarray来访问这些整数。在这种情况下,我们可以围绕这个内存创建一个数组,并告诉numpy有两个整数,它们是16位和大端:

>>> import numpy as np
>>> big_end_arr = np.ndarray(shape=(2,),dtype='>i2', buffer=big_end_str)
>>> big_end_arr[0]
1
>>> big_end_arr[1]
770

请注意dtype上的数据类型>i2>表示'big-endian'(<是小端),i2表示'带符号的2字节整数'。例如,如果我们的数据表示一个无符号的4字节little-endian整数,则dtype字符串应该是<u4

事实上,我们为什么不尝试呢?

>>> little_end_u4 = np.ndarray(shape=(1,),dtype='<u4', buffer=big_end_str)
>>> little_end_u4[0] == 1 * 256**1 + 3 * 256**2 + 2 * 256**3
True

回到我们的big_end_arr - 在这种情况下,我们的基础数据是big-endian(数据字节顺序),我们设置了dtype匹配(dtype也是big-endian)。但是,有时你需要翻转这些。

警告!

标量当前不包含字节顺序信息,因此从数组中提取标量将以本机字节顺序返回一个整数。因此:

>>> big_end_arr[0].dtype.byteorder == little_end_u4[0].dtype.byteorder
True

# 更改字节顺序

正如你从介绍可以想象的,有两种方式可以影响数组的字节顺序和它正在查看的底层内存之间的关系:

  • 更改数组dtype中的字节顺序信息,以便它将未确定的数据解释为处于不同的字节顺序。这是arr.newbyteorder()的作用
  • 改变底层数据的字节顺序,保持原来的dtype解释。这是arr.byteswap()所做的。

您需要更改字节顺序的常见情况是:

  1. 您的数据和dtype字尾不匹配,并且您想要更改dtype以使其与数据匹配。
  2. 您的数据和dtype字尾不匹配,您想要交换数据以便它们与dtype匹配
  3. 你的数据和dtype的字节匹配,但你想要交换数据和dtype来反映这一点

# 数据和dtype字节顺序不匹配,将dtype更改为匹配数据

我们做一些他们不匹配的东西:

>>> wrong_end_dtype_arr = np.ndarray(shape=(2,),dtype='<i2', buffer=big_end_str)
>>> wrong_end_dtype_arr[0]
256

这种情况的明显解决方法是更改​​dtype,以便提供正确的排列顺序:

>>> fixed_end_dtype_arr = wrong_end_dtype_arr.newbyteorder()
>>> fixed_end_dtype_arr[0]
1

请注意数组在内存中没有改变:

>>> fixed_end_dtype_arr.tobytes() == big_end_str
True

# 数据和类型字节顺序不匹配,更改数据以匹配dtype

如果您需要将内存中的数据设置为特定顺序,则可能需要执行此操作。例如,您可能将内存写入需要某个字节排序的文件。

>>> fixed_end_mem_arr = wrong_end_dtype_arr.byteswap()
>>> fixed_end_mem_arr[0]
1

现在数组在内存中有更改:

>>> fixed_end_mem_arr.tobytes() == big_end_str
False

# 数据和dtype字节顺序匹配,交换数据和dtype

你可能为一个数组指定了正确的dtype,但你需要数组在内存中有相反的字节顺序,你想让dtype匹配,所以数组值是有意义的。在这种情况下,您只需执行以前的两个操作:

>>> swapped_end_arr = big_end_arr.byteswap().newbyteorder()
>>> swapped_end_arr[0]
1
>>> swapped_end_arr.tobytes() == big_end_str
False

使用ndarray astype方法可以实现将数据转换为特定dtype和字节顺序的更简单的方法:

>>> swapped_end_arr = big_end_arr.astype('<i2')
>>> swapped_end_arr[0]
1
>>> swapped_end_arr.tobytes() == big_end_str
False

结构化数组

# 介绍

结构化数组其实就是ndarrays,其数据类型是由组成一系列命名字段的简单数据类型组成的。 例如:

>>> x = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
...              dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f4')])
>>> x
array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
      dtype=[('name', 'S10'), ('age', '<i4'), ('weight', '<f4')])

这里x是长度为2的一维数组,其数据类型是具有三个字段的结构:1、名为'name'的长度为10或更小的字符串。2、名为'age'的32位整数。3、名为'weight'的32位浮点数。

如果你的索引x是1,你会看到这样的结构:

>>> x[1]
('Fido', 3, 27.0)

你可以通过使用字段名称进行索引来访问和修改结构化数组的各个字段的值:

>>> x['age']
array([9, 3], dtype=int32)
>>> x['age'] = 5
>>> x
array([('Rex', 5, 81.0), ('Fido', 5, 27.0)],
      dtype=[('name', 'S10'), ('age', '<i4'), ('weight', '<f4')])

结构化数组设计用于结构化数据的底层操作,例如解释编译二进制数据块。结构化数据类型旨在模仿C语言中的 “structs”,使它们对于与C代码接口也很有用。 为了达到这些目的,numpy支持诸如子阵列和嵌套数据类型之类的特殊功能,并允许手动控制结构的内存布局。

如果你想进行表格数据的简单操作,那么其他 pydata 项目(例如pandas,xarray或DataArray)将为你提供更适合的更高级别的接口。 这些包也可以为表格数据分析提供更好的性能,因为NumPy中的结构化数组的类C结构内存布局会导致缓存行为不佳。

# 结构化数据类型

要使用结构化数组,首先需要定义结构化数据类型。

结构化数据类型可以被认为是一定长度的字节序列(结构的itemsize),它被解释为一个字段集合。 每个字段在结构中都有一个名称,一个数据类型和一个字节偏移量。 字段的数据类型可以是任何numpy数据类型,包括其他结构化数据类型,它也可以是一个子数组,其行为类似于指定形状的ndarray。 字段的偏移是任意的,并且字段甚至可以重叠。 这些偏移通常由numpy自动确定,但也可以指定。

# 结构化数据类型创建

结构化数据类型可以使用函数numpy.dtype创建。 有4种可选形式的规范,其灵活性和简洁性各不相同。 这些在数据类型对象参考页面中都有进一步的记录,总之它们是:

  1. 元组列表,每个字段一个元组

    每个元组都有这些属性(fieldname,datatype,shape),其中shape是可选的。 fieldname是一个字符串(或元组,如果使用标题,请参阅下面的字段标题),datatype可以是任何可转换为数据类型的对象,shape是指定子阵列形状的整数元组。

     >>> np.dtype([('x', 'f4'), ('y', np.float32), ('z', 'f4', (2,2))])
     dtype=[('x', '<f4'), ('y', '<f4'), ('z', '<f4', (2, 2))])

    如果fieldname是空字符串'',那么该字段将被赋予一个默认名称形式f#,其中#是该字段的整数索引,从左边以0开始计数:

     >>> np.dtype([('x', 'f4'),('', 'i4'),('z', 'i8')])
     dtype([('x', '<f4'), ('f1', '<i4'), ('z', '<i8')])

    结构中字段的字节偏移量和总体结构中元素的大小是自动确定的。

  2. 一串用逗号分隔的dtype规范

    在这种简写表示法中,任何 string dtype specifications 都可以在字符串中使用逗号分隔,字段的元素大小(itemsize)和字节偏移量是自动确定的,并且字段名称被赋予默认名称如":“f0”、“f1” 等。

     >>> np.dtype('i8,f4,S3')
     dtype([('f0', '<i8'), ('f1', '<f4'), ('f2', 'S3')])
     >>> np.dtype('3int8, float32, (2,3)float64')
     dtype([('f0', 'i1', 3), ('f1', '<f4'), ('f2', '<f8', (2, 3))])
  3. 字段参数数组的字典

    这是最灵活的规范形式,因为它允许控制字段的字节偏移量和结构中的元素大小(itemsize)。

    字典有两个必需的键,'names' 和 'formats',以及四个可选键,'offsets','itemsize','aligned' 和 'titles'。 'names' 和 'formats' 的值应该分别是长度相同的字段名称列表和dtype规范列表。 可选的 'offsets' 值应该是整数字节偏移量的列表,结构中的每个字段都有一个偏移量。 如果没有给出 'offsets',则自动确定偏移量。 可选的 'itemsize' 值应该是一个描述dtype的总大小(以字节为单位)的整数,它必须足够大以包含所有字段。

     >>> np.dtype({'names': ['col1', 'col2'], 'formats': ['i4','f4']})
     dtype([('col1', '<i4'), ('col2', '<f4')])
     >>> np.dtype({'names': ['col1', 'col2'],
     ...           'formats': ['i4','f4'],
     ...           'offsets': [0, 4],
     ...           'itemsize': 12})
     dtype({'names':['col1','col2'], 'formats':['<i4','<f4'], 'offsets':[0,4], 'itemsize':12})

    可以选择偏移使得字段重叠,但这意味着分配给一个字段可能破坏任何重叠字段的数据。 作为一个例外,numpy.object类型的字段不能与其他字段重叠,因为存在破坏内部对象指针然后解除引用的风险。

    可选的 “aligned” 值可以设置为True,以使自动偏移计算使用对齐的偏移(请参阅自动字节偏移和对齐),就好像numpy.dtype的'align'关键字参数已设置为True。

    可选的 “titles” 值应该是与 “names” 长度相同的标题列表,请参阅的字段标题

  4. 字段名称的字典

    不鼓励使用这种形式的规范,但也在此列出,因为较旧的numpy代码可能会使用它。字典的键是字段名称,值是指定类型和偏移量的元组:

     >>> np.dtype=({'col1': ('i1',0), 'col2': ('f4',1)})
     dtype([(('col1'), 'i1'), (('col2'), '>f4')])

    不鼓励使用这种形式,因为Python的字典类型在Python3.6之前没有保留Python版本中的顺序,并且结构化dtype中字段的顺序具有意义。字段标题可以通过使用3元组来指定,请参见下面的内容.

# 操作和显示结构化数据类型

可以在dtype对象的names属性中找到结构化数据类型的字段名称列表:

>>> d = np.dtype([('x', 'i8'), ('y', 'f4')])
>>> d.names
('x', 'y')

可以通过使用相同长度的字符串序列分配 names 属性来修改字段名称。

dtype对象还有一个类似字典的属性fields,其键是字段名称(和Field Titles,见下文),其值是包含每个字段的dtype和byte偏移量的元组。

>>> d.fields
mappingproxy({'x': (dtype('int64'), 0), 'y': (dtype('float32'), 8)})

对于非结构化数组,namesfields 属性都是 None

如果可能,结构化数据类型的字符串表示形式为“元组列表”的形式,否则numpy将回退到使用更通用的字典的形式。

# 自动字节偏移和对齐

Numpy使用两种方法中的一个来自动确定字节字节偏移量和结构化数据类型的整体项目大小,具体取决于是否将align = True指定为numpy.dtype的关键字参数。

默认情况下(align = False),numpy将字段打包在一起,使得每个字段从前一个字段结束的字节偏移开始,并且字段在内存中是连续的。

>>> def print_offsets(d):
...     print("offsets:", [d.fields[name][1] for name in d.names])
...     print("itemsize:", d.itemsize)
>>> print_offsets(np.dtype('u1,u1,i4,u1,i8,u2'))
offsets: [0, 1, 2, 6, 7, 15]
itemsize: 17

如果设置align = True,numpy将以与许多C编译器填充C结构相同的方式填充结构。 在某些情况下,对齐结构可以提高性能,但代价是增加了数据的大小。 在字段之间插入填充字节,使得每个字段的字节偏移量将是该字段对齐的倍数,对于简单数据类型,通常等于字段的字节大小,请参阅“PyArray_Descr.alignment”。 该结构还将添加尾随填充,以使其itemsize是最大字段对齐的倍数。

>>> print_offsets(np.dtype('u1,u1,i4,u1,i8,u2', align=True))
offsets: [0, 1, 4, 8, 16, 24]
itemsize: 32

请注意,尽管默认情况下几乎所有现代C编译器都以这种方式填充,但C结构中的填充依赖于C实现,因此不能保证此内存布局与C程序中相应结构的内容完全匹配。 为了获得确切的对应关系,可能需要在numpy或C这边进行一些工作。

如果在基于字典的dtype规范中使用可选的offsets键指定了偏移量,设置align = True将检查每个字段的偏移量是否为其大小的倍数,项大小是否为最大字段大小的倍数,如果不是,则引发异常。

如果结构化数组的字段和项目大小的偏移满足对齐条件,则数组将设置 ALIGNED 标志。

便捷函数numpy.lib.recfunctions.repack_fields将对齐的dtype或数组转换为已打包的dtype或数组,反之亦然。它接受dtype或结构化ndarray作为参数,并返回一个带有重新打包的字段的副本,无论是否有填充字节。

# 字段标题

除了字段名称之外,字段还可以具有关联的标题,备用名称,有时用作字段的附加说明或别名。 标题可用于索引数组,就像字段名一样。

要在使用dtype规范的list-of-tuples形式时添加标题,可以将字段名称指定为两个字符串的元组而不是单个字符串,它们分别是字段的标题和字段名称。 例如:

>>> np.dtype([(('my title', 'name'), 'f4')])

当使用第一种形式的基于字典的规范时,标题可以作为额外的“标题”作为键提供,如上所述。 当使用第二个(不鼓励的)基于字典的规范时,可以通过提供3元素元组(数据类型,偏移量,标题)而不是通常的2元素元组来提供标题:

>>> np.dtype({'name': ('i4', 0, 'my title')})

如果使用了标题,dtype.field字典将包含作为键的标题。这意味着具有标题的字段将在字段字典中表示两次。这些字段的元组值还有第三个元素,字段标题。因此,由于name属性保留了字段顺序,而field属性可能不能,建议使用dtype的name属性迭代dtype的字段,该属性不会列出标题,如下所示:

>>> for name in d.names:
...     print(d.fields[name][:2])

# 联合类型

结构化数据类型在numpy中实现,默认情况下具有基类型numpy.void,但是可以使用数据类型对象中描述的dtype规范的(base_dtype,dtype)形式将其他numpy类型解释为结构化类型。 这里,base_dtype是所需的底层dtype,字段和标志将从dtype复制。 这个dtype类似于C中的'union'。

# 结构化数组的索引和分配

# 将数据分配给结构化数组

有许多方法可以为结构化数组赋值:使用python元组、使用标量值或使用其他结构化数组。

# 使用Python原生类型(元组)来赋值

将值赋给结构化数组的最简单方法是使用python元组。 每个赋值应该是一个长度等于数组中字段数的元组,而不是列表或数组,因为它们将触发numpy的广播规则。 元组的元素从左到右分配给数组的连续字段:

>>> x = np.array([(1,2,3),(4,5,6)], dtype='i8,f4,f8')
>>> x[1] = (7,8,9)
>>> x
array([(1, 2., 3.), (7, 8., 9.)],
     dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '<f8')])

# 通过标量赋值

分配给结构化元素的标量将分配给所有字段。 将标量分配给结构化数组时,或者将非结构化数组分配给结构化数组时,会发生这种情况:

>>> x = np.zeros(2, dtype='i8,f4,?,S1')
>>> x[:] = 3
>>> x
array([(3, 3.0, True, b'3'), (3, 3.0, True, b'3')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])
>>> x[:] = np.arange(2)
>>> x
array([(0, 0.0, False, b'0'), (1, 1.0, True, b'1')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])

结构化数组也可以分配给非结构化数组,但前提是结构化数据类型只有一个字段:

>>> twofield = np.zeros(2, dtype=[('A', 'i4'), ('B', 'i4')])
>>> onefield = np.zeros(2, dtype=[('A', 'i4')])
>>> nostruct = np.zeros(2, dtype='i4')
>>> nostruct[:] = twofield
ValueError: Can't cast from structure to non-structure, except if the structure only has a single field.
>>> nostruct[:] = onefield
>>> nostruct
array([0, 0], dtype=int32)

# 来自其他结构化数组的赋值

两个结构化数组之间的分配就像源元素已转换为元组然后分配给目标元素一样。 也就是说,源阵列的第一个字段分配给目标数组的第一个字段,第二个字段同样分配,依此类推,而不管字段名称如何。 具有不同数量的字段的结构化数组不能彼此分配。 未包含在任何字段中的目标结构的字节不受影响。

>>> a = np.zeros(3, dtype=[('a', 'i8'), ('b', 'f4'), ('c', 'S3')])
>>> b = np.ones(3, dtype=[('x', 'f4'), ('y', 'S3'), ('z', 'O')])
>>> b[:] = a
>>> b
array([(0.0, b'0.0', b''), (0.0, b'0.0', b''), (0.0, b'0.0', b'')],
      dtype=[('x', '<f4'), ('y', 'S3'), ('z', 'O')])

# 子阵列的赋值

分配给子阵列的字段时,首先将指定的值广播到子阵列的形状。

# 索引结构化数组

# 访问单个字段

可以通过使用字段名称索引数组来访问和修改结构化数组的各个字段。

>>> x = np.array([(1,2),(3,4)], dtype=[('foo', 'i8'), ('bar', 'f4')])
>>> x['foo']
array([1, 3])
>>> x['foo'] = 10
>>> x
array([(10, 2.), (10, 4.)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

可以通过使用字段名称索引数组来访问和修改结构化数组的各个字段。

>>> y = x['bar']
>>> y[:] = 10
>>> x
array([(10, 5.), (10, 5.)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

此视图与索引字段具有相同的dtype和itemsize,因此它通常是非结构化数组,但嵌套结构除外。

>>> y.dtype, y.shape, y.strides
(dtype('float32'), (2,), (12,))

# 访问多个字段

可以索引并分配具有多字段索引的结构化数组,其中索引是字段名称列表

警告

多字段索引的说明将从Numpy 1.14升级Numpy 1.15。

在Numpy 1.15中,使用多字段索引进行索引的结果将是原始数组的视图,如下所示:

>>> a = np.zeros(3, dtype=[('a', 'i4'), ('b', 'i4'), ('c', 'f4')])
>>> a[['a', 'c']]
array([(0, 0.), (0, 0.), (0, 0.)],
     dtype={'names':['a','c'], 'formats':['<i4','<f4'], 'offsets':[0,8], 'itemsize':12})

对视图的赋值会修改原始数组。 视图的字段将按索引编号的顺序排列。 请注意,与单字段索引不同,视图的dtype与原始数组具有相同的项目大小,并且具有与原始数组中相同的偏移量的字段,并且仅缺少未编入索引的字段。

在Numpy 1.14中,索引具有多字段索引的数组将返回上述结果的副本(对于1.15),但将字段打包在内存中,就好像通过了numpy.lib.recFunctions.repack_field。这是Numpy 1.7到1.13的行为。

警告

Numpy 1.15中的新行为导致在未索引字段的位置出现额外的“填充”字节。您将需要更新所有的代码,这取决于具有“打包”布局的数据。例如下面的代码:

>>> a[['a','c']].view('i8')  # will fail in Numpy 1.15
ValueError: When changing to a smaller dtype, its size must be a divisor of the size of original dtype

需要升级。这段代码从Numpy 1.12开始引发了 FutureWarning 的错误

以下是修复性建议,下面这段代码在Numpy 1.14和Numpy 1.15中的作用相同:

>>> from numpy.lib.recfunctions import repack_fields
>>> repack_fields(a[['a','c']]).view('i8')  # supported 1.14 and 1.15
array([0, 0, 0])

赋值给具有多字段索引的数组在Numpy 1.14和Numpy 1.15中的作用相同。在两个版本中,赋值都将修改原始数组:

>>> a[['a', 'c']] = (2, 3)
>>> a
array([(2, 0, 3.0), (2, 0, 3.0), (2, 0, 3.0)],
      dtype=[('a', '<i8'), ('b', '<i4'), ('c', '<f8')])

这遵循上述结构化阵列赋值规则。 例如,这意味着可以使用适当的多字段索引交换两个字段的值:

>>> a[['a', 'c']] = a[['c', 'a']]

# 用整数索引获取结构化标量

索引结构化数组的单个元素(带有整数索引)返回结构化标量:

>>> x = np.array([(1, 2., 3.)], dtype='i,f,f')
>>> scalar = x[0]
>>> scalar
(1, 2., 3.)
>>> type(scalar)
numpy.void

与其他数值标量不同的是,结构化标量是可变的,并且像原始数组中的视图一样,因此修改标量将修改原始数组。结构化标量还支持按字段名进行访问和赋值:

>>> x = np.array([(1,2),(3,4)], dtype=[('foo', 'i8'), ('bar', 'f4')])
>>> s = x[0]
>>> s['bar'] = 100
>>> x
array([(1, 100.), (3, 4.)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

与元组类似,结构化标量也可以用整数索引:

>>> scalar = np.array([(1, 2., 3.)], dtype='i,f,f')[0]
>>> scalar[0]
1
>>> scalar[1] = 4

因此,元组可能被认为是原生Python中等同于numpy的结构化的类型,就像原生python整数相当于numpy的整数类型。 可以通过调用ndarray.item将结构化标量转换为元组:

>>> scalar.item(), type(scalar.item())
((1, 2.0, 3.0), tuple)

# 查看包含对象的结构化数组

为了防止在`numpy.Object类型的字段中阻塞对象指针,numpy目前不允许包含对象的结构化数组的视图。

# 结构比较

如果两个空结构数组的dtype相等,则测试数组的相等性将生成一个具有原始数组的维度的布尔数组,元素设置为True,其中相应结构的所有字段都相等。如果字段名称、dtype和标题相同,而忽略endianness,且字段的顺序相同,则结构化dtype是相等的:

>>> a = np.zeros(2, dtype=[('a', 'i4'), ('b', 'i4')])
>>> b = np.ones(2, dtype=[('a', 'i4'), ('b', 'i4')])
>>> a == b
array([False, False])

目前,如果两个void结构化数组的dtypes不相等,则比较失败返回标量值“False”。 从numpy 1.10开始不推荐使用这种方式,并且这种方式将来会引发错误或执行元素比较。

The < and > operators always return False when comparing void structured arrays, and arithmetic and bitwise operations are not supported.

在比较空结构数组时,<> 操作符总是返回 False,并且不支持算术和位运算符。

# 记录数组

作为一个可选的方便的选项,numpy提供了一个ndarray子类 numpy.recarray,以及 numpy.rec子模块中的相关帮助函数,它允许通过属性访问结构化数组的字段,而不仅仅是通过索引。记录数组还使用一种特殊的数据类型 numpy.Record,它允许通过属性对从数组获得的结构化标量进行字段访问。

创建记录数组的最简单方法是使用 numpy.rec.array, 像下面这样:

>>> recordarr = np.rec.array([(1,2.,'Hello'),(2,3.,"World")],
...                    dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')])
>>> recordarr.bar
array([ 2.,  3.], dtype=float32)
>>> recordarr[1:2]
rec.array([(2, 3.0, 'World')],
      dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])
>>> recordarr[1:2].foo
array([2], dtype=int32)
>>> recordarr.foo[1:2]
array([2], dtype=int32)
>>> recordarr[1].baz
'World'

numpy.rec.array 可以将各种参数转换为记录数组,包括结构化数组:

>>> arr = array([(1,2.,'Hello'),(2,3.,"World")],
...             dtype=[('foo', 'i4'), ('bar', 'f4'), ('baz', 'S10')])
>>> recordarr = np.rec.array(arr)

numpy.rec模块提供了许多其他便捷的函数来创建记录数组,请参阅记录数组创建API

可以使用适当的视图获取结构化数组的记录数组表示:

>>> arr = np.array([(1,2.,'Hello'),(2,3.,"World")],
...                dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'a10')])
>>> recordarr = arr.view(dtype=dtype((np.record, arr.dtype)),
...                      type=np.recarray)

为方便起见,查看类型为np.recarray的ndarray会自动转换为np.record数据类型,因此dtype可以不在视图之外:

>>> recordarr = arr.view(np.recarray)
>>> recordarr.dtype
dtype((numpy.record, [('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')]))

要返回普通的ndarray,必须重置dtype和type。 以下视图是这样做的,考虑到recordarr不是结构化类型的异常情况:

>>> arr2 = recordarr.view(recordarr.dtype.fields or recordarr.dtype, np.ndarray)

如果字段具有结构化类型,则返回由index或by属性访问的记录数组字段作为记录数组,否则返回普通ndarray。

>>> recordarr = np.rec.array([('Hello', (1,2)),("World", (3,4))],
...                 dtype=[('foo', 'S6'),('bar', [('A', int), ('B', int)])])
>>> type(recordarr.foo)
<type 'numpy.ndarray'>
>>> type(recordarr.bar)
<class 'numpy.core.records.recarray'>

请注意,如果字段与ndarray属性具有相同的名称,则ndarray属性优先。 这些字段将无法通过属性访问,但仍可通过索引访问。

子类化ndarray

# 介绍

子类化ndarray相对简单,但与其他Python对象相比,它有一些复杂性。 在这个页面上,我们解释了允许你子类化ndarray的机制,以及实现子类的含义。

# ndarrays和对象创建

ndarray的子类化很复杂,因为ndarray类的新实例可以以三种不同的方式出现。 这些是:

  1. 显式构造函数调用 - 如MySubClass(params)。 这是创建Python实例的常用途径。
  2. 查看转换 - 将现有的ndarray转换为给定的子类。
  3. 从模板创建新实例-从模板实例创建新实例。示例包括从子类数组返回片、从uFuncs创建返回类型和复制数组。有关更多详细信息,请参见从模板创建

最后两个是ndarrays的特征 - 为了支持数组切片之类的东西。 子类化ndarray的复杂性是由于numpy必须支持后两种实例创建路径的机制。

# 视图投影

视图投影是标准的ndarray机制,通过它您可以获取任何子类的ndarray,并将该数组的视图作为另一个(指定的)子类返回:

>>> import numpy as np
>>> # create a completely useless ndarray subclass
>>> class C(np.ndarray): pass
>>> # create a standard ndarray
>>> arr = np.zeros((3,))
>>> # take a view of it, as our useless subclass
>>> c_arr = arr.view(C)
>>> type(c_arr)
<class 'C'>

# 从模版创建

当numpy发现它需要从模板实例创建新实例时,ndarray子类的新实例也可以通过与视图转换非常相似的机制来实现。 这个情况的最明显的时候是你正为子类阵列切片的时候。例如:

>>> v = c_arr[1:]
>>> type(v) # the view is of type 'C'
<class 'C'>
>>> v is c_arr # but it's a new instance
False

切片是原始 C_ARR 数据的视图。因此,当我们从ndarray获取视图时,我们返回一个新的ndarray,它属于同一个类,指向原始的数据。

在使用ndarray时,我们还需要这样的视图,比如复制数组(C_arr.Copy())、创建ufunc输出数组(关于uFunc函数和其他函数,也请参阅arraywarp___ )和简化方法(比如 C_arr.Means() )。

# 视图投影和从模版创建的关系

这些路径都使用相同的机制。我们在这里进行区分,因为它们会为您的方法产生不同的输入。 具体来说,View转换意味着您已从ndarray的任何潜在子类创建了数组类型的新实例。从模板创建新意味着您已从预先存在的实例创建了类的新实例,例如,允许您跨特定于您的子类的属性进行复制。

# 子类化的含义

如果我们继承ndarray,我们不仅需要处理数组类型的显式构造,还需要处理视图投影或从模板创建。NumPy有这样的机制,这种机制使子类略微不标准。

ndarray用于支持视图和子类中的new-from-template(从模版创建)的机制有两个方面。

第一个是使用ndarray .__ new__方法进行对象初始化的主要工作,而不是更常用的__init__方法。 第二个是使用__array_finalize__方法,允许子类在创建视图和模板中的新实例后进行内存清除。

# 关于在Python中的 __new____init__ 的简短入门

__ new__是一个标准的Python方法,如果存在,在创建类实例时在__init__之前调用。 有关更多详细信息,请参阅 python new文档

例如,请思考以下Python代码:

class C(object):
    def __new__(cls, *args):
        print('Cls in __new__:', cls)
        print('Args in __new__:', args)
        return object.__new__(cls, *args)

    def __init__(self, *args):
        print('type(self) in __init__:', type(self))
        print('Args in __init__:', args)

思考后我们可以得到:

>>> c = C('hello')
Cls in __new__: <class 'C'>
Args in __new__: ('hello',)
type(self) in __init__: <class 'C'>
Args in __init__: ('hello',)

当我们调用C('hello')时,__ new__方法将自己的类作为第一个参数,并传递参数,即字符串'hello'。 在python调用 __new__ 之后,它通常(见下文)调用我们的 __init__ 方法,将__new__ 的输出作为第一个参数(现在是一个类实例),然后传递参数。

正如你所看到的,对象可以在__new__`方法或__init__方法中初始化,或两者兼而有之,实际上ndarray没有__init__方法,因为所有的初始化都是 在__new__方法中完成。

为什么要使用__new__而不是通常的__init__? 因为在某些情况下,对于ndarray,我们希望能够返回其他类的对象。 考虑以下:

class D(C):
    def __new__(cls, *args):
        print('D cls is:', cls)
        print('D args in __new__:', args)
        return C.__new__(C, *args)

    def __init__(self, *args):
        # we never get here
        print('In D __init__')

实践后:

>>> obj = D('hello')
D cls is: <class 'D'>
D args in __new__: ('hello',)
Cls in __new__: <class 'C'>
Args in __new__: ('hello',)
>>> type(obj)
<class 'C'>

C的定义与之前相同,但对于D__new__方法返回类C而不是D的实例。 请注意,D__init__方法不会被调用。 通常,当__new__方法返回除定义它的类之外的类的对象时,不调用该类的__init__方法。

这就是ndarray类的子类如何能够返回保留类类型的视图。 在观察时,标准的ndarray机器创建了新的ndarray对象,例如:

obj = ndarray.__new__(subtype, shape, ...

其中subdtype是子类。 因此,返回的视图与子类属于同一类,而不是类ndarray

这解决了返回相同类型视图的问题,但现在我们遇到了一个新问题。 ndarray的机制可以用这种方式设置类,在它的标准方法中获取视图,但是ndarray__new__方法不知道我们在自己的__new__方法中做了什么来设置属性, 等等。 (旁白 - 为什么不调用obj = subdtype .__ new __(...那么?因为我们可能没有一个带有相同调用特征的__new__`方法)。

# __array_finalize__ 的作用

__array_finalize__是numpy提供的机制,允许子类处理创建新实例的各种方法。

请记住,子类实例可以通过以下三种方式实现:

  1. 显式构造函数调用(obj = MySubClass(params))。 这将调用通常的MySubClass .__ new__方法,然后再调用(如果存在)MySubClass .__ init__
  2. 视图投影。
  3. 从模板创建。

我们的MySubClass .__ new__方法仅在显式构造函数调用的情况下被调用,因此我们不能依赖于MySubClass .__ new__MySubClass .__ init__来处理视图投影和“从模板创建”。 事实证明,对于所有三种对象创建方法都会调用MySubClass .__ array_finalize__,所以这就是我们的对象从内部创建理通常会发生的情况。

  • 对于显式构造函数调用,我们的子类需要创建自己的类的新ndarray实例。 在实践中,这意味着我们作为代码的编写者,需要调用ndarray .__ new __(MySubClass,...),一个类的层次结构会调用super(MySubClass,cls) .__ new __(cls,...),或查看现有数组的视图投影(见下文)
  • 对于视图投影和“从模板创建”,相当于ndarray .__ new __(MySubClass,...在C级的调用。
    array_finalize收到的参数因上面三种实例创建方法而异。

以下代码允许我们查看调用顺序和参数:

import numpy as np

class C(np.ndarray):
    def __new__(cls, *args, **kwargs):
        print('In __new__ with class %s' % cls)
        return super(C, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        # in practice you probably will not need or want an __init__
        # method for your subclass
        print('In __init__ with class %s' % self.__class__)

    def __array_finalize__(self, obj):
        print('In array_finalize:')
        print('   self type is %s' % type(self))
        print('   obj type is %s' % type(obj))

现在看:

>>> # Explicit constructor
>>> c = C((10,))
In __new__ with class <class 'C'>
In array_finalize:
   self type is <class 'C'>
   obj type is <type 'NoneType'>
In __init__ with class <class 'C'>
>>> # View casting
>>> a = np.arange(10)
>>> cast_a = a.view(C)
In array_finalize:
   self type is <class 'C'>
   obj type is <type 'numpy.ndarray'>
>>> # Slicing (example of new-from-template)
>>> cv = c[:1]
In array_finalize:
   self type is <class 'C'>
   obj type is <class 'C'>

__array_finalize__ 的特征是:

def __array_finalize__(self, obj):

可以看到super调用,它转到ndarray .__ new__,将__array_finalize__传递给我们自己的类(self)的新对象以及来自的对象 视图已被采用(obj)。 从上面的输出中可以看出,self始终是我们子类的新创建的实例,而obj的类型对于三个实例创建方法是不同的:

  • 当从显式构造函数调用时,“obj”是“None”。
  • 当从视图转换调用时,obj可以是ndarray的任何子类的实例,包括我们自己的子类。
  • 当在新模板中调用时,obj是我们自己子类的另一个实例,我们可以用它来更新的self实例。

因为__array_finalize__是唯一始终看到正在创建新实例的方法,所以它是填充新对象属性的实例的默认值以及其他任务的合适的位置。

通过示例可能更清楚。

# 简单示例 - 向ndarray添加额外属性

import numpy as np

class InfoArray(np.ndarray):

    def __new__(subtype, shape, dtype=float, buffer=None, offset=0,
                strides=None, order=None, info=None):
        # Create the ndarray instance of our type, given the usual
        # ndarray input arguments.  This will call the standard
        # ndarray constructor, but return an object of our type.
        # It also triggers a call to InfoArray.__array_finalize__
        obj = super(InfoArray, subtype).__new__(subtype, shape, dtype,
                                                buffer, offset, strides,
                                                order)
        # set the new 'info' attribute to the value passed
        obj.info = info
        # Finally, we must return the newly created object:
        return obj

    def __array_finalize__(self, obj):
        # ``self`` is a new object resulting from
        # ndarray.__new__(InfoArray, ...), therefore it only has
        # attributes that the ndarray.__new__ constructor gave it -
        # i.e. those of a standard ndarray.
        #
        # We could have got to the ndarray.__new__ call in 3 ways:
        # From an explicit constructor - e.g. InfoArray():
        #    obj is None
        #    (we're in the middle of the InfoArray.__new__
        #    constructor, and self.info will be set when we return to
        #    InfoArray.__new__)
        if obj is None: return
        # From view casting - e.g arr.view(InfoArray):
        #    obj is arr
        #    (type(obj) can be InfoArray)
        # From new-from-template - e.g infoarr[:3]
        #    type(obj) is InfoArray
        #
        # Note that it is here, rather than in the __new__ method,
        # that we set the default value for 'info', because this
        # method sees all creation of default objects - with the
        # InfoArray.__new__ constructor, but also with
        # arr.view(InfoArray).
        self.info = getattr(obj, 'info', None)
        # We do not need to return anything

使用该对象如下所示:

>>> obj = InfoArray(shape=(3,)) # explicit constructor
>>> type(obj)
<class 'InfoArray'>
>>> obj.info is None
True
>>> obj = InfoArray(shape=(3,), info='information')
>>> obj.info
'information'
>>> v = obj[1:] # new-from-template - here - slicing
>>> type(v)
<class 'InfoArray'>
>>> v.info
'information'
>>> arr = np.arange(10)
>>> cast_arr = arr.view(InfoArray) # view casting
>>> type(cast_arr)
<class 'InfoArray'>
>>> cast_arr.info is None
True

这个类不是很有用,因为它与裸ndarray对象具有相同的构造函数,包括传入缓冲区和形状等等。我们可能更偏向于希望构造函数能够将已经构成的ndarray类型通过常用的numpy的np.array来调用并返回一个对象。

# 更真实的示例 - 添加到现有数组的属性

这是一个类,它采用已经存在的标准ndarray,转换为我们的类型,并添加一个额外的属性。

import numpy as np

class RealisticInfoArray(np.ndarray):

    def __new__(cls, input_array, info=None):
        # Input array is an already formed ndarray instance
        # We first cast to be our class type
        obj = np.asarray(input_array).view(cls)
        # add the new attribute to the created instance
        obj.info = info
        # Finally, we must return the newly created object:
        return obj

    def __array_finalize__(self, obj):
        # see InfoArray.__array_finalize__ for comments
        if obj is None: return
        self.info = getattr(obj, 'info', None)

所以:

>>> arr = np.arange(5)
>>> obj = RealisticInfoArray(arr, info='information')
>>> type(obj)
<class 'RealisticInfoArray'>
>>> obj.info
'information'
>>> v = obj[1:]
>>> type(v)
<class 'RealisticInfoArray'>
>>> v.info
'information'

# ufuncs的__array_ufunc__

版本1.13中的新功能。

子类可以通过重写默认的ndarray.__arrayufunc_方法来重写在其上执行numpy uFunc函数时的行为。此方法将代替ufunc执行,如果未实现所请求的操作,则应返回操作结果或NotImplemented`

__array_ufunc__ 的特征是:

def __array_ufunc__(ufunc, method, *inputs, **kwargs):

- *ufunc* 是被调用的ufunc对象。
- *method* 是一个字符串,指示如何调用Ufunc。“__call__” 表示它是直接调用的,或者是下面的其中一个:`methods <ufuncs.methods>`:“reduce”,“accumulate”,“reduceat”,“outer” 或 “at” 等属性。
- *inputs* 是 “ufunc” 的类型为元组的输入参数。
- *kwargs* A包含传递给函数的任何可选或关键字参数。 这包括任何``out``参数,并且它们总是包含在元组中。

典型的实现将转换任何作为自己类的实例的输入或输出,使用super()方法将所有内容传递给超类,最后在可能的反向转换后返回结果。 从core / tests / test_umath.py中的测试用例test_ufunc_override_with_super获取的示例如下。

input numpy as np

class A(np.ndarray):
    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
        args = []
        in_no = []
        for i, input_ in enumerate(inputs):
            if isinstance(input_, A):
                in_no.append(i)
                args.append(input_.view(np.ndarray))
            else:
                args.append(input_)

        outputs = kwargs.pop('out', None)
        out_no = []
        if outputs:
            out_args = []
            for j, output in enumerate(outputs):
                if isinstance(output, A):
                    out_no.append(j)
                    out_args.append(output.view(np.ndarray))
                else:
                    out_args.append(output)
            kwargs['out'] = tuple(out_args)
        else:
            outputs = (None,) * ufunc.nout

        info = {}
        if in_no:
            info['inputs'] = in_no
        if out_no:
            info['outputs'] = out_no

        results = super(A, self).__array_ufunc__(ufunc, method,
                                                 *args, **kwargs)
        if results is NotImplemented:
            return NotImplemented

        if method == 'at':
            if isinstance(inputs[0], A):
                inputs[0].info = info
            return

        if ufunc.nout == 1:
            results = (results,)

        results = tuple((np.asarray(result).view(A)
                         if output is None else output)
                        for result, output in zip(results, outputs))
        if results and isinstance(results[0], A):
            results[0].info = info

        return results[0] if len(results) == 1 else results

所以,这个类实际上没有做任何有趣的事情:它只是将它自己的任何实例转换为常规的ndarray(否则,我们会得到无限的递归!),并添加一个info字典,告诉哪些输入和输出它转换。因此,例如:

>>> a = np.arange(5.).view(A)
>>> b = np.sin(a)
>>> b.info
{'inputs': [0]}
>>> b = np.sin(np.arange(5.), out=(a,))
>>> b.info
{'outputs': [0]}
>>> a = np.arange(5.).view(A)
>>> b = np.ones(1).view(A)
>>> c = a + b
>>> c.info
{'inputs': [0, 1]}
>>> a += b
>>> a.info
{'inputs': [0, 1], 'outputs': [0]}

注意,另一种方法是使用 getattr(ufunc, methods)(*inputs, **kwargs) 而不是调用 super 。对于此示例,结果将是相同的,但如果另一个运算数也定义 __array_ufunc__ 则会存在差异。 例如,假设我们执行 np.add(a, b),其中b是另一个重写功能的b类的实例。如果你在示例中调用 superndarray .__ array_ufunc__ 会注意到b有一个覆盖,这意味着它无法评估结果本身。因此,它将返回NotImplemented,我们的类A也将返回。然后,控制权将传递给b,它知道如何处理我们并生成结果,或者不知道并返回NotImplemented,从而引发TypeError

如果相反,我们用 getattr(ufunc, method) 替换我们的 Super 调用,那么我们实际上执行了np.add(a.view(np.ndarray), b)。同样,b.arrayufunc_将被调用,但现在它将一个ndarray作为另一个参数。很可能,它将知道如何处理这个问题,并将B类的一个新实例返回给我们。我们的示例类并不是为处理这个问题而设置的,但是,如果要使用___array_`___ufunc_重新实现 MaskedArray,那么它很可能是最好的方法。

最后要注意:如果走super的路线适合给定的类,使用它的一个优点是它有助于构造类层次结构。例如,假设我们的其他类B在其__array_ufunc__实现中也使用了super,我们创建了一个依赖于它们的类C,即class C (A,B)(为简单起见,不是另一个__array_ufunc__覆盖)。 那么C实例上的任何ufunc都会传递给A .__ array_ufunc__A中的super调用将转到B .__ array_ufunc__,并且 B中的super调用将转到ndarray .__ array_ufunc__,从而允许A`进行协作。

# __array_wrap__用于ufuncs和其他函数

在numpy 1.13之前,ufuncs的行为只能使用__array_wrap__`__array_prepare__`进行调整。这两个允许一个更改ufunc的输出类型,但是,与前两者相反,__array_ufunc__`,它不允许对输入进行任何更改。它希望最终弃能弃用这些功能,但是__array_wrap__也被其他numpy函数和方法使用,比如squeeze,所以目前仍需要完整的功能。

从概念上讲,__array_wrap__`“包含动作”是允许子类设置返回值的类型并更新属性和元数据。让我们用一个例子来说明这是如何工作的。 首先,我们返回更简单的示例子类,但使用不同的名称和一些print语句:

import numpy as np

class MySubClass(np.ndarray):

    def __new__(cls, input_array, info=None):
        obj = np.asarray(input_array).view(cls)
        obj.info = info
        return obj

    def __array_finalize__(self, obj):
        print('In __array_finalize__:')
        print('   self is %s' % repr(self))
        print('   obj is %s' % repr(obj))
        if obj is None: return
        self.info = getattr(obj, 'info', None)

    def __array_wrap__(self, out_arr, context=None):
        print('In __array_wrap__:')
        print('   self is %s' % repr(self))
        print('   arr is %s' % repr(out_arr))
        # then just call the parent
        return super(MySubClass, self).__array_wrap__(self, out_arr, context)

我们在新数组的实例上运行ufunc:

>>> obj = MySubClass(np.arange(5), info='spam')
In __array_finalize__:
   self is MySubClass([0, 1, 2, 3, 4])
   obj is array([0, 1, 2, 3, 4])
>>> arr2 = np.arange(5)+1
>>> ret = np.add(arr2, obj)
In __array_wrap__:
   self is MySubClass([0, 1, 2, 3, 4])
   arr is array([1, 3, 5, 7, 9])
In __array_finalize__:
   self is MySubClass([1, 3, 5, 7, 9])
   obj is MySubClass([0, 1, 2, 3, 4])
>>> ret
MySubClass([1, 3, 5, 7, 9])
>>> ret.info
'spam'

请注意,ufunc (np.add) 调用了__array_wack__``方法,其参数 ``self`` 作为 ``obj``,``out_arr为该加法的(ndarray)结果。反过来,默认的 __array_wirp_ (ndarray.arraray_wirp_) 已将结果转换为类MySubClass,名为 _array_radline_` - 因此复制了info 属性。这一切都发生在C级。

但是,我们可以做任何我们想做的事:

class SillySubClass(np.ndarray):

    def __array_wrap__(self, arr, context=None):
        return 'I lost your data'
>>> arr1 = np.arange(5)
>>> obj = arr1.view(SillySubClass)
>>> arr2 = np.arange(5)
>>> ret = np.multiply(obj, arr2)
>>> ret
'I lost your data'

因此,通过为我们的子类定义一个特定的__array_wrap__方法,我们可以调整ufuncs的输出。 __array_wrap__方法需要self,然后是一个参数 - 这是ufunc的结果 - 和一个可选的参数上下文。 ufuncs将此参数作为3元素元组返回:( ufunc的名称,ufunc的参数,ufunc的域),但不是由其他numpy函数设置的。 但是,如上所述,可以这样做,__ array_wrap__应返回其包含类的实例。 有关实现,请参阅masked数组子类。

除了在退出ufunc时调用的 __array_wrap__ 之外,还存在一个 __array_prepare__ 方法,该方法在创建输出数组之后但在执行任何计算之前,在进入ufunc的过程中被调用。默认实现除了传递数组之外什么都不做。__array_prepare__ 不应该尝试访问数组数据或调整数组大小,它的目的是设置输出数组类型,更新属性和元数据,并根据在计算开始之前需要的输入执行任何检查。与 __array_wrap__ 一样,__array_prepare__ 必须返回一个ndarray或其子类,或引发一个错误。

# 额外的坑 - 自定义__del__方法 和 ndarray.base

darray解决的问题之一是跟踪ndarray的内存所有权和它们的视图。考虑这样一个例子:我们创建了一个ndarray,arr,并用 v=arr[1:] 取了一个切片。这两个对象看到的是相同的内存。NumPy使用 base 属性跟踪特定数组或视图的数据来源:

>>> # A normal ndarray, that owns its own data
>>> arr = np.zeros((4,))
>>> # In this case, base is None
>>> arr.base is None
True
>>> # We take a view
>>> v1 = arr[1:]
>>> # base now points to the array that it derived from
>>> v1.base is arr
True
>>> # Take a view of a view
>>> v2 = v1[1:]
>>> # base points to the view it derived from
>>> v2.base is v1
True

一般来说,如果数组拥有自己的内存,就像在这种情况下的 arr 一样,那么 arr.base 将是None - 这方面有一些例外 - 更多细节请参见 Numpy 的书籍。

base属性可以告诉我们是否有视图或原始数组。 如果我们需要知道在删除子类数组时是否进行某些特定的清理,这反过来会很有用。 例如,如果删除原始数组,我们可能只想进行清理,而不是视图。 有关它如何工作的示例,请查看numpy.core中的memmap类。

# 子类和下游兼容性

当对ndarray进行子类化或创建模仿ndarray接口的duck-types时,你有责任决定你的API与numpy的对齐方式。 为方便起见,许多具有相应ndarray方法的numpy函数(例如,summeantakereshape)都会检查第一个参数,看是否一个函数有一个同名的方法。如果是,则调用该方法,反之则将参数强制转换为numpy数组。

例如,如果你希望子类或duck-type与numpy的sum函数兼容,则此对象的sum`方法的方法特征应如下所示:

def sum(self, axis=None, dtype=None, out=None, keepdims=False):
...

这是np.sum的完全相同的方法特征,所以现在如果用户在这个对象上调用np.sum,numpy将调用该对象自己的sum方法并传入这些参数,在特征上枚举,并且不会引起任何错误,因为他们的特征彼此完全兼容。

但是,如果您决定偏离相关特征并执行以下操作:

def sum(self, axis=None, dtype=None):
...

这个对象不再与np.sum兼容,因为如果你调用np.sum,它将传递意外的参数outkeepdims,导致引发TypeError的错误。

如果你希望保持与numpy及其后续版本(可能会添加新的关键字参数)的兼容性,但又不想显示所有numpy的参数,那么你的函数的特征应该接受 ** kwargs。 例如:

def sum(self, axis=None, dtype=None, **unused_kwargs):
...

此对象现在再次与np.sum兼容,因为任何无关的参数(即不是axisdtype的关键字)将被隐藏在** unused_kwargs参数中。

0

原文地址:https://www.cnblogs.com/smurfs/p/9670765.html