Python科学计算(第2版)
上QQ阅读APP看书,第一时间看更新

2.1.6 结构数组

在C语言中我们可以通过struct关键字定义结构类型,结构中的字段占据连续的内存空间。类型相同的两个结构所占用的内存大小相同,因此可以很容易定义结构数组。和C语言一样,在NumPy中也很容易对这种结构数组进行操作。只要NumPy中的结构定义和C语言中的结构定义相同,就可以很方便地读取C语言的结构数组的二进制数据,将其转换为NumPy的结构数组。

假设我们需要定义一个结构数组,它的每个元素都有name、age和weight字段。在NumPy中可以如下定义:

    persontype = np.dtype({ ❶
        'names':['name', 'age', 'weight'],
        'formats':['S30','i', 'f']}, align=True)
    a = np.array([("Zhang", 32, 75.5), ("Wang", 24, 65.2)], ❷
        dtype=persontype)

❶我们先创建一个dtype对象persontype,它的参数是一个描述结构类型的各个字段的字典。字典有两个键:'names'和'formats'。每个键对应的值都是一个列表。'names'定义结构中每个字段的名称,而'formats'则定义每个字段的类型。这里我们使用类型字符串定义字段类型:

●'S30':长度为30个字节的字符串类型,由于结构中的每个元素的大小必须固定,因此需要指定字符串的长度。

●'i':32位的整数类型,相当于np.int32。

●'f':32位的单精度浮点数类型,相当于np.float32。

❷然后调用array()以创建数组,通过dtype参数指定所创建的数组的元素类型为persontype。下面查看数组a的元素类型:

    a.dtype
    dtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'], 
        'offsets':[0,32,36], 'itemsize':40}, align=True)

还可以用包含多个元组的列表来描述结构的类型:

    dtype([('name', '|S30'), ('age', '<i4'), ('weight', '<f4')])

其中形如“(字段名,类型描述)”的元组描述了结构中的每个字段。类型字符串前面的'|'、'<'、'>'等字符表示字段值的字节顺序:

●|:忽视字节顺序。

●lt;:低位字节在前,即小端模式(little endian)。

●>:高位字节在前,即大端模式(big endian)。

结构数组的存取方式和一般数组相同,通过下标能够取得其中的元素,注意元素的值看上去像是元组,实际上是结构:

    print a[0]
    a[0].dtype
    ('Zhang', 32, 75.5)
    dtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'], 
        'offsets':[0,32,36], 'itemsize':40}, align=True)

我们可以使用字段名作为下标获取对应的字段值:

    a[0]["name"]
    'Zhang'

a[0]是一个结构元素,它和数组a共享内存数据,因此可以通过修改它的字段来改变原始数组中对应元素的字段:

    c = a[1]
    c["name"] = "Li"
    a[1]["name"]
    'Li'

我们不但可以获得结构元素的某个字段,而且可以直接获得结构数组的字段,返回的是原始数组的视图,因此可以通过修改b[0]来改变a[0]["age"]:

    b=a["age"]
    b[0] = 40
    print a[0]["age"]
    40

通过a.tostring()或a.tofile()方法,可以将数组a以二进制的方式转换成字符串或写入文件:

    a.tofile("test.bin")

利用下面的C语言程序可以将test.bin文件中的数据读取出来。%%file为IPython的魔法命令,它将该单元格中的文本保存成文件read_struct_array.c:

    %%file read_struct_array.c
    #include <stdio.h>
    
    struct person 
    {
        char name[30];
        int age;
        float weight;
    };
    
    struct person p[3];
    
    void main ()
    {
        FILE *fp;
        int i;
        fp=fopen("test.bin","rb");
        fread(p, sizeof(struct person), 2, fp);
        fclose(fp);
        for(i=0;i<2;i++)
        {
            printf("%s %d %f\n", p[i].name, p[i].age, p[i].weight);
        }
    }

在IPython中可以通过!执行系统命令,下面调用gcc编译前面的C语言程序并执行:

    !gcc read_struct_array.c -o read_struct_array.exe
    !read_struct_array.exe
    Zhang 40 75.500000
    Li 24 65.199997

内存对齐

为了内存寻址方便,C语言的结构类型会自动添加一些填充用的字节,这叫做内存对齐。例如上面C语言中定义的结构的name字段虽然是30个字节长,但是由于内存对齐问题,在name和age中间会填补两个字节。因此,如果数组中所配置的内存大小不符合C语言的对齐规范,将会出现数据错位。为了解决这个问题,在创建dtype对象时,可以传递参数align=True,这样结构数组的内存对齐就和C语言的结构类型一致了。在前面的例子中,由于创建persontype时指定align参数为True,因此它占用40个字节。

结构类型中可以包括其他的结构类型,下面的语句创建一个有一个字段f1的结构,f1的值是另一个结构,它有字段f2,类型为16位整数:

    np.dtype([('f1', [('f2', np.int16)])])
    dtype([('f1', [('f2', '<i2')])])

当某个字段类型为数组时,用元组的第三个元素表示其形状。在下面的结构体中,f1字段是一个形状为(2, 3)的双精度浮点数组:

    np.dtype([('f0', 'i4'), ('f1', 'f8', (2, 3))])
    dtype([('f0', '<i4'), ('f1', '<f8', (2, 3))])

用下面的字典参数也可以定义结构类型,字典的键为结构的字段名,值为字段的类型描述。但是由于字典的键是没有顺序的,因此字段的顺序需要在类型描述中给出。类型描述是一个元组,它的第二个值给出字段的以字节为单位的偏移量,例如下例中的age字段的偏移量为25个字节:

    np.dtype({'surname':('S25',0),'age':(np.uint8,25)})
    dtype([('surname', 'S25'), ('age', 'u1')])