numpy数据结构

under numpy

in tech

Published: 2017-02-03

numpy是一个Python高效数值运算库,高效的代价是其数值计算核心要用C和Fortran实现,而在使用numpy时,可能有必要了解更底层的知识,特别是涉及I/O操作的应用。

多维数组(ndarray)是numpy的基础类型,是对多个相同类型的数据的封装,ndarray对这些数据的描述分为两部分:类型和维度,类型描述了每个单元数据表示什么,维度描述这些单元数据之间的相互关系。

dtype

numpy里面的数据类型(data type)术语是dtype,它是对多维数组中单元数据的抽象,在dtype之上,一个整数数组和一个浮点数数组的维度变换和broadcast操作没有任何区别,涉及单元数据的运算时,才需要知道每个单元数据的确切含义。

dtype的任务就是解释单元数据表示的含义,它可以是一个数、一个数值数组、一个字符串或者这些成员的组合,甚至可以包含其它dtype对象,举个例子:

 t1                        t1     t1     t1
  |                         |      |      |
  V                         V      V      V
[ int | float array | t2 ][ ... ][ ... ][ ... ] ...
                      |
                      V
                    [ string | complex ]

其中t1是包含3个成员,第一个成员是一个整数,第二个成员是一个浮点数组,第三个成员是另一个由字符串和复数组成的数据类型t2。

基础类型

从上面的例子可以看出,dtype用到了整数、浮点数、字符串等基本元素来描述数据类型,这些基本元素来自于numpy定义的一套类型系统,以numpy.generic为根,形成一个树形结构,比如numpy 1.8.2定义了以下类型(省略numpy.前缀):

可以看出,numpy支持布尔、字符和数这些常用类型,其中数分为整数、浮点数和复数,并根据精度进一步细分,其后缀数字即该类型的总位数。例如float32,表示该类型的浮点数占32位,即4字节。

继承了Python内置类型的类型右侧括号里标出了详细的继承关系,例如string_类型,从树形结构中的位置来看,当然继承自character,但它同时也继承自Python内置类型str

名字中带_后缀的类型,经测试有以下关系成立:

from numpy import (bool_,
                   complex_,
                   dtype,
                   float_,
                   int_,
                   object_,
                   str_,
                   string_,
                   unicode_)
dtype(bool_) == dtype(bool)
dtype(string_) == dtype(str_) == dtype(str)
dtype(unicode_) == dtype(unicode)
dtype(complex_) == dtype(complex)
dtype(float_) == dtype(float)
dtype(int_) == dtype(int)
dtype(object_) == dtype(object)

除了bool_object_,这些类型都继承自相应的Python内置类型。

令人迷惑的是,numpy这个名空间里,也定义了boolcomplexfloatintobjectstrunicode等名字,但它们只是Python内置变量的别名。

dtype和基础类型的区别

dtype对象和基础类型都可以用来指定数组的数据类型,比如以下两个整数数组的定义都是合法的:

x = np.array([1, 2, 3], np.int32)
y = np.array([1, 2, 3], dtype(np.int32))

但两者在概念上完全不同,基础类型是Python类,例如上面定义的数组x的每个元素都是np.int32的实例。np.int32本身也可以实例化:np.int32(5)

dtype对象不是类型,而是dtype类的实例,其作用是描述数组中每个元素的内存结构。通过dtype处理新类型的数组时,只要定义新的dtype对象,而不需要创建新类。

如果指定的类型不是dtype对象,可以视作用dtype构造函数将其转化成一个dtype对象,dtype构造函数接受的描述方式非常多样化,为了减轻记忆负担并维持风格的统一,这里提供一种实用的dtype描述方法,举例如下:

避免使用像"int32"这样的字符串,更要避免使用"i4"这样的简写,因为字符串容易打错,不能自动补齐。

在指定比较复杂的结构时,使用list,尽量不要使用dict类型,因为后者无法自动确定结构各成员的先后顺序,要手动输入每个成员的内存地址偏移量,容易出错并且难以向已定义的结构体中插入新成员,也要避免使用(S4,f8)这样的字符串,原因同上。

结构体

可以看到,numpy可以非常方便地定义结构体,并且结构体的嵌套也很容易实现,比如以下C结构体:

typedef struct {
    int difficulty;
    int performance;
} _score;

typedef struct {
    char name[256];
    char sex[10];
    _score score[20];
} _player;

可以被直观地表示为:

score_type = np.dtype([("difficulty", np.int32),
                       ("performance", np.int32)])
player_type = np.dtype([("name", np.string_, 256),
                        ("sex", np.string_, 10),
                        ("score", score_type, 20)])

该结构体的每个成员都可以被单独取出:

x = np.ndarray((2, 2), dtype=player_type)
x['name']  # 2x2 array
x['score']['difficulty']  # 2x2x20 array

结构体数组可以看成一个数组,其每个成员都具有复杂的结构,或者看成多个数组嵌套组合在一起,其每个成员都具有相同的维度。从这个意义上说,结构本身也可以看成一个独立的维度(在numpy里确实如此,并且可以用整数来索引结构体成员),只是这个维度可以用成员的名字来索引,在用名字索引时,该维度出现的位置并不重要,x['name'][1]x[1]['name']是一样的。

numpy还提供了recarray类型,可以通过属性来获取结构体成员,但这些属性是动态的,还无法做到自动补齐,在我看来,只是少打几个符号而已。