PyTables 快速上手#

本章由一系列简单但全面的教程组成,将使您能够理解 PyTables 的主要功能。

请注意,在整个文档中,术语“列”(column)和“字段”(field)将互换使用,术语“行”(row)和“记录”(record)也将互换使用。

声明列描述符#

假设有一个粒子探测器,希望创建表对象以保存从中检索到的数据。首先,需要定义表、它有多少列、每列包含什么类型的对象等等。

粒子探测器有动态范围为 8 位的 TDC(时间到数字转换器)计数器和范围为 16 位的 ADC(模拟到数字转换器)。对于这些值,将在记录对象中定义两个字段,分别称为 TDCcount 和 ADCcount。还希望保存检测到粒子的网格位置,因此将添加两个新字段,分别称为 grid_igrid_j。仪器还可以获得粒子的压力和能量。压力计的分辨率允许我们使用单精度浮点数来存储压力读数,而能量值将需要双精度浮点数。最后,为了跟踪粒子,希望为其分配名称以识别粒子的种类,并分配唯一的数字标识符。因此,将再添加两个字段:name 将是一个最多 16 个字符的字符串,idnumber 将是一个 64 位的整数(以允许我们为极大量的粒子存储记录)。

确定了列及其类型后,现在可以声明新的 Particle 类,其中包含所有这些信息:

from tables import *

class Particle(IsDescription):
    name      = StringCol(16)   # 16-character String
    idnumber  = Int64Col()      # Signed 64-bit integer
    ADCcount  = UInt16Col()     # Unsigned short integer
    TDCcount  = UInt8Col()      # unsigned byte
    grid_i    = Int32Col()      # 32-bit integer
    grid_j    = Int32Col()      # 32-bit integer
    pressure  = Float32Col()    # float  (single-precision)
    energy    = Float64Col()    # double (double-precision)

这个定义类是自解释的。基本上,您为每个需要的字段声明一个类变量。作为其值,您根据定义的列类型(数据类型、长度、形状等)分配适当的 Col 子类的实例。有关这些子类的完整描述,请参见 The Col class and its descendants。另请参见 Supported data types in PyTables,了解 Col 构造函数支持的数据类型列表。

从现在开始,可以使用 Particle 实例作为探测器数据表的描述符。稍后将看到如何传递此对象来构造表。但首先,必须创建文件,所有实际数据都将保存到表中。

从头创建 PyTables 文件#

使用顶级 open_file() 函数创建 PyTables 文件:

from pathlib import Path

temp_dir = Path(".temp")
temp_dir.mkdir(exist_ok=True)
h5file = open_file(temp_dir/"tutorial1.h5", mode="w", title="Test file")

open_file() 是由 from tables import * 语句导入的对象之一。在这里,表示要在当前工作目录中以“w”rite 模式创建名为“tutorial1.h5”的新文件,并带有描述性标题字符串(“Test file”)。此函数尝试打开文件,如果成功,则返回 File(参见 The File Class)对象实例 h5file。对象树的根在实例的 root 属性中指定。

h5file
File(filename=.temp/tutorial1.h5, title='Test file', mode='w', root_uep='/', filters=Filters(complevel=0, shuffle=False, bitshuffle=False, fletcher32=False, least_significant_digit=None))
/ (RootGroup) 'Test file'

创建新组#

现在,为了更好地组织数据,将从根节点创建名为 detector 的组。将把粒子数据表保存在这个组中:

group = h5file.create_group("/", 'detector', 'Detector information')

在这里,获取了 File 实例 h5file,并调用其 create_group() 方法,从“/”(另一种引用上面提到的 h5file.root 对象的方式)创建名为 detector 的新组。这将创建新的 Group(参见 The Group class)对象实例,并将其分配给变量 group

group
/detector (Group) 'Detector information'
  children := []
h5file.root
/ (RootGroup) 'Test file'
  children := ['detector' (Group)]

创建新表#

现在,在新创建的组下创建 Table(参见 The Table class)对象。通过调用 create_table() 方法来实现这一点:

table = h5file.create_table(group, 'readout', Particle, "Readout example")
table
/detector/readout (Table(0,)) 'Readout example'
  description := {
  "ADCcount": UInt16Col(shape=(), dflt=0, pos=0),
  "TDCcount": UInt8Col(shape=(), dflt=0, pos=1),
  "energy": Float64Col(shape=(), dflt=0.0, pos=2),
  "grid_i": Int32Col(shape=(), dflt=0, pos=3),
  "grid_j": Int32Col(shape=(), dflt=0, pos=4),
  "idnumber": Int64Col(shape=(), dflt=0, pos=5),
  "name": StringCol(itemsize=16, shape=(), dflt=b'', pos=6),
  "pressure": Float32Col(shape=(), dflt=0.0, pos=7)}
  byteorder := 'little'
  chunkshape := (1394,)

group 下创建 Table 实例。我们为这个表分配节点名称“readout”。前面声明的 Particle 类是描述参数(用于定义表的列),最后将“Readout example”设置为 Table 标题。有了所有这些信息,就会创建新的 Table 实例并分配给变量 table

如果您对对象树现在的样子感到好奇,只需打印实例变量 h5file,并检查输出:

print(h5file)
.temp/tutorial1.h5 (File) 'Test file'
Last modif.: '2024-12-03T09:38:05+00:00'
Object Tree: 
/ (RootGroup) 'Test file'
/detector (Group) 'Detector information'
/detector/readout (Table(0,)) 'Readout example'

正如您所见,显示了对象树的转储。很容易看到我们刚刚创建的 GroupTable 对象。如果您想要更多信息,只需键入包含 File 实例的变量:

h5file
File(filename=.temp/tutorial1.h5, title='Test file', mode='w', root_uep='/', filters=Filters(complevel=0, shuffle=False, bitshuffle=False, fletcher32=False, least_significant_digit=None))
/ (RootGroup) 'Test file'
/detector (Group) 'Detector information'
/detector/readout (Table(0,)) 'Readout example'
  description := {
  "ADCcount": UInt16Col(shape=(), dflt=0, pos=0),
  "TDCcount": UInt8Col(shape=(), dflt=0, pos=1),
  "energy": Float64Col(shape=(), dflt=0.0, pos=2),
  "grid_i": Int32Col(shape=(), dflt=0, pos=3),
  "grid_j": Int32Col(shape=(), dflt=0, pos=4),
  "idnumber": Int64Col(shape=(), dflt=0, pos=5),
  "name": StringCol(itemsize=16, shape=(), dflt=b'', pos=6),
  "pressure": Float32Col(shape=(), dflt=0.0, pos=7)}
  byteorder := 'little'
  chunkshape := (1394,)

在树状结构中,每个对象的详细信息都被显示出来。注意观察我们的表描述符类 Particle 是如何作为读出表描述信息的一部分被打印出来的。通常来说,通过简单地打印对象及其子对象,你可以获得更多关于它们的信息。这种自省能力非常有用,我推荐你广泛使用它。

现在是时候给这个表格填充一些值了。首先我们将获取到这个表格实例的 Row(参见 The Row 类)实例的指针:

particle = table.row
type(particle)
tables.tableextension.Row

tablerow 属性指向 Row 实例,该实例将用于将数据行写入表中。通过将 Row 实例的值分配给每一行,就像它是一个字典(尽管它实际上是扩展类),使用列名作为键来写入数据。

以下是如何写入行的示例:

for i in range(10):
    particle['name']  = f'Particle: {i:6d}'
    particle['TDCcount'] = i % 256
    particle['ADCcount'] = (i * 256) % (1 << 16)
    particle['grid_i'] = i
    particle['grid_j'] = 10 - i
    particle['pressure'] = float(i*i)
    particle['energy'] = float(particle['pressure'] ** 4)
    particle['idnumber'] = i * (2 ** 34)
    # Insert a new particle record
    particle.append()

这段代码应该很容易理解。循环内的行只是将值分配给 Row 实例 particle 中的不同列(参见 The Row class)。调用其 append() 方法将此信息写入表 I/O 缓冲区。

在处理完所有数据后,如果我们想将所有这些数据写入磁盘,我们应该刷新表的 I/O 缓冲区。我们通过调用 flush() 方法来实现这一点:

table.flush()

请记住,刷新表是非常重要的步骤,因为它不仅有助于保持文件的完整性,还可以释放宝贵的内存资源(即内部缓冲区),您的程序可能需要这些资源来执行其他操作。

读取(和选择)表中的数据#

好的。数据已经在磁盘上,现在需要访问它并从特定列中选择感兴趣的值。请参见以下示例:

table = h5file.root.detector.readout
pressure = [x['pressure'] for x in table.iterrows() if x['TDCcount'] > 3 and 20 <= x['pressure'] < 50]
pressure
[25.0, 36.0, 49.0]

第一行创建了指向对象树中更深处的 readout 表的“快捷方式”。正如您所见,使用自然命名模式来访问它。也可以使用 get_node() 方法,稍后将这样做。

您会认出最后两行是 Python 列表推导式。它循环遍历由 iterrows() 迭代器提供的 table 中的行。迭代器返回值,直到 table 中的所有数据都被耗尽。这些行使用以下表达式进行过滤:

x['TDCcount'] > 3 and 20 <= x['pressure'] < 50

因此,正在从过滤后的记录中选择 pressure 列的值,以创建最终列表并将其分配给 pressure 变量。

本可以使用普通的 for 循环来实现相同的目的,但推导式语法更紧凑和优雅。

PyTables 提供了其他更强大的选择方法,如果您有非常大的表或需要非常高的查询速度,这些方法可能更合适。它们被称为内核内查询和索引查询,您可以通过 where() 和其他相关方法使用它们。

让我们使用内核内选择来查询 name 列以获取相同的一组过滤条件:

names = [ x['name'] for x in table.where("""(TDCcount > 3) & (20 <= pressure) & (pressure < 50)""") ]
names
[b'Particle:      5', b'Particle:      6', b'Particle:      7']

内核内查询和索引查询不仅速度更快,而且如您所见,它们看起来也更紧凑,是 PyTables 最伟大的功能之一,因此请确保您经常使用它们。有关内核内查询和索引选择的更多信息,请参见条件语法加速搜索

备注

在查询条件中包含字符串字面量时,应特别注意。实际上,Python 2 的字符串字面量是字节字符串,而 Python 3 的字符串是 Unicode 对象。

参考上述 Particle 的定义,需要注意的是,“name”列的类型不会根据使用的 Python 版本而改变(当然)。它始终对应于字节字符串。

任何涉及“name”列的条件都应使用字符串字面量的适当类型,以避免 TypeError。

假设要获取对应于特定粒子名称的行。

下面的代码在 Python 2 中可以正常工作,但在 Python 3 中会引发 TypeError:

condition = '(name == "Particle:      5") | (name == "Particle:      7")'
for record in table.where(condition):  # Python3 中会引发 TypeError
    # 对 "record" 进行某些操作

原因是,在 Python 3 中,“condition” 涉及字节字符串(“name”列内容)和 Unicode 字面量之间的比较。

正确的条件写法是:

condition = '(name == b"Particle:      5") | (name == b"Particle:      7")'

关于选择的内容就介绍到这里。接下来的部分将向你展示如何将这些选定的结果保存到文件中。

创建新数组对象#

为了将选定的数据与大量探测器数据分开,将从根组创建名为 columns 的新组。之后,在这个组下,将创建两个数组,其中包含选定的数据。首先,创建组:

gcolumns = h5file.create_group(h5file.root, "columns", "Pressure and Name")

请注意,这次使用自然命名(h5file.root)而不是绝对路径字符串("/")指定了第一个参数。

现在,创建刚刚提到的两个 Array 对象中的第一个:

import numpy as np
h5file.create_array(gcolumns, 'pressure', np.array(pressure), "Pressure column selection")
/columns/pressure (Array(3,)) 'Pressure column selection'
  atom := Float64Atom(shape=(), dflt=0.0)
  maindim := 0
  flavor := 'numpy'
  byteorder := 'little'
  chunkshape := None

已经知道 create_array() 方法的前两个参数(这与 create_table() 的前两个参数相同):它们是将在其中创建 Array 的父组和 Array 实例名称。第三个参数是我们希望保存到磁盘的对象。在这种情况下,它是由我们之前创建的选择列表构建的 NumPy 数组。第四个参数是标题。

现在,将保存第二个数组。它包含我们之前选择的字符串列表:按原样保存此对象,无需进一步转换:

h5file.create_array(gcolumns, 'name', names, "Name column selection")
/columns/name (Array(3,)) 'Name column selection'
  atom := StringAtom(itemsize=16, shape=(), dflt=b'')
  maindim := 0
  flavor := 'python'
  byteorder := 'irrelevant'
  chunkshape := None

如您所见,create_array() 接受名称(这是常规的 Python 列表)作为对象参数。实际上,它接受各种不同的常规对象作为参数。flavor 属性(参见上面的输出)保存了保存的原始对象类型。基于此 flavor,PyTables 将能够在以后从磁盘中检索完全相同的对象。

请注意,在这些示例中,create_array() 方法返回的 Array 实例未分配给任何变量。别担心,这是故意的,以显示创建的对象类型,方法是显示其表示。如您所见,Array 对象已附加到对象树并保存到磁盘:

print(h5file)
.temp/tutorial1.h5 (File) 'Test file'
Last modif.: '2024-12-03T09:38:05+00:00'
Object Tree: 
/ (RootGroup) 'Test file'
/columns (Group) 'Pressure and Name'
/columns/name (Array(3,)) 'Name column selection'
/columns/pressure (Array(3,)) 'Pressure column selection'
/detector (Group) 'Detector information'
/detector/readout (Table(10,)) 'Readout example'

关闭文件并查看其内容#

为了完成这个第一个教程,在退出 Python 之前使用 h5file 对象的 close 方法关闭文件:

h5file.close()

您现在已经创建了第一个包含表和两个数组的 PyTables 文件。您可以使用任何通用的 HDF5 工具(如 h5dumph5ls)检查它。