用于在内存中维护表格数据的数据结构?


77

我的情况如下:我有一个数据表(少数字段,少于一百行),该数据表在程序中广泛使用。我还需要这些数据具有持久性,因此我将其另存为CSV并在启动时加载。我选择不使用数据库,因为每个选项(甚至是SQLite)对于我的卑微要求都是过高的(另外-我希望能够以一种简单的方式离线编辑值,没有什么比记事本更简单的了)。

假设我的数据如下所示(在文件中用逗号分隔,没有标题,这只是一个例证):

 Row  | Name     | Year   | Priority
------------------------------------
 1    | Cat      | 1998   | 1
 2    | Fish     | 1998   | 2
 3    | Dog      | 1999   | 1 
 4    | Aardvark | 2000   | 1
 5    | Wallaby  | 2000   | 1
 6    | Zebra    | 2001   | 3

笔记:

  1. 行可以是写入文件的“实际”值,也可以是代表行号的自动生成的值。无论哪种方式,它都存在于内存中。
  2. 名称是唯一的。

我对数据所做的事情:

  1. 根据ID(迭代)或名称(直接访问)查找一行。
  2. 根据多个字段,以不同的顺序显示表格:我需要对表格进行排序,例如,按优先级然后按年份排序,或者按年份然后按优先级排序,等等
  3. 我需要根据参数集对实例进行计数,例如,1997年至2002年之间的年份有多少行,或者1998年有多少行并且优先级> 2,等等。

我知道SQL的这种“呼声” ...

我试图找出什么是数据结构的最佳选择。以下是我看到的几种选择:

行列表列表:

a = []
a.append( [1, "Cat", 1998, 1] )
a.append( [2, "Fish", 1998, 2] )
a.append( [3, "Dog", 1999, 1] )
...

列列表的列表(显然会有add_row等的API):

a = []
a.append( [1, 2, 3, 4, 5, 6] )
a.append( ["Cat", "Fish", "Dog", "Aardvark", "Wallaby", "Zebra"] )
a.append( [1998, 1998, 1999, 2000, 2000, 2001] )
a.append( [1, 2, 1, 1, 1, 3] )

列列表字典(可以创建常量来替换字符串键):

a = {}
a['ID'] = [1, 2, 3, 4, 5, 6]
a['Name'] = ["Cat", "Fish", "Dog", "Aardvark", "Wallaby", "Zebra"] 
a['Year'] = [1998, 1998, 1999, 2000, 2000, 2001] 
a['Priority'] = [1, 2, 1, 1, 1, 3] 

键为(Row,Field)元组的字典:

Create constants to avoid string searching
NAME=1
YEAR=2
PRIORITY=3

a={}
a[(1, NAME)] = "Cat"
a[(1, YEAR)] = 1998
a[(1, PRIORITY)] = 1
a[(2, NAME)] = "Fish"
a[(2, YEAR)] = 1998
a[(2, PRIORITY)] = 2
...

而且我敢肯定还有其他方法...但是,在满足我的要求(复杂的订购和计数)时,每种方法都有缺点。

推荐的方法是什么?

编辑:

需要澄清的是,性能对我而言不是主要问题。由于表太小,我相信几乎每个操作都将在毫秒范围内,这对我的应用程序来说不是问题。


3
无需阅读答案。您的问题为我的问题提供了答案!非常感谢您提出这样一个很棒的问题:)
Gajendra Bagali

Answers:


80

在内存中有一个需要查询,排序和任意聚合的“表”确实确实需要SQL。您说您尝试过SQLite,但是您是否意识到SQLite可以使用仅内存数据库?

connection = sqlite3.connect(':memory:')

然后,您可以使用SQLite的所有功能在内存中创建/删除/查询/更新表,完成后不留文件。从Python 2.5开始,sqlite3它在标准库中,因此它并不是真正的“过度杀伤” IMO。

这是一个示例如何创建和填充数据库的示例:

import csv
import sqlite3

db = sqlite3.connect(':memory:')

def init_db(cur):
    cur.execute('''CREATE TABLE foo (
        Row INTEGER,
        Name TEXT,
        Year INTEGER,
        Priority INTEGER)''')

def populate_db(cur, csv_fp):
    rdr = csv.reader(csv_fp)
    cur.executemany('''
        INSERT INTO foo (Row, Name, Year, Priority)
        VALUES (?,?,?,?)''', rdr)

cur = db.cursor()
init_db(cur)
populate_db(cur, open('my_csv_input_file.csv'))
db.commit()

如果您真的不想使用SQL,则应该使用字典列表:

lod = [ ] # "list of dicts"

def populate_lod(lod, csv_fp):
    rdr = csv.DictReader(csv_fp, ['Row', 'Name', 'Year', 'Priority'])
    lod.extend(rdr)

def query_lod(lod, filter=None, sort_keys=None):
    if filter is not None:
        lod = (r for r in lod if filter(r))
    if sort_keys is not None:
        lod = sorted(lod, key=lambda r:[r[k] for k in sort_keys])
    else:
        lod = list(lod)
    return lod

def lookup_lod(lod, **kw):
    for row in lod:
        for k,v in kw.iteritems():
            if row[k] != str(v): break
        else:
            return row
    return None

然后测试得出:

>>> lod = []
>>> populate_lod(lod, csv_fp)
>>> 
>>> pprint(lookup_lod(lod, Row=1))
{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'}
>>> pprint(lookup_lod(lod, Name='Aardvark'))
{'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'}
>>> pprint(query_lod(lod, sort_keys=('Priority', 'Year')))
[{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'},
 {'Name': 'Dog', 'Priority': '1', 'Row': '3', 'Year': '1999'},
 {'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'},
 {'Name': 'Wallaby', 'Priority': '1', 'Row': '5', 'Year': '2000'},
 {'Name': 'Fish', 'Priority': '2', 'Row': '2', 'Year': '1998'},
 {'Name': 'Zebra', 'Priority': '3', 'Row': '6', 'Year': '2001'}]
>>> pprint(query_lod(lod, sort_keys=('Year', 'Priority')))
[{'Name': 'Cat', 'Priority': '1', 'Row': '1', 'Year': '1998'},
 {'Name': 'Fish', 'Priority': '2', 'Row': '2', 'Year': '1998'},
 {'Name': 'Dog', 'Priority': '1', 'Row': '3', 'Year': '1999'},
 {'Name': 'Aardvark', 'Priority': '1', 'Row': '4', 'Year': '2000'},
 {'Name': 'Wallaby', 'Priority': '1', 'Row': '5', 'Year': '2000'},
 {'Name': 'Zebra', 'Priority': '3', 'Row': '6', 'Year': '2001'}]
>>> print len(query_lod(lod, lambda r:1997 <= int(r['Year']) <= 2002))
6
>>> print len(query_lod(lod, lambda r:int(r['Year'])==1998 and int(r['Priority']) > 2))
0

我个人更喜欢SQLite版本,因为它可以更好地保留您的类型(在Python中无需额外的转换代码)并且可以轻松扩展以适应将来的需求。但是话又说回来,我对SQL非常满意,所以对YMMV来说很满意。


2
在此示例中,推荐的填充数据库的方法是什么?
罗伊·阿德勒

4
我已经编辑了答案,以包括示例代码以从CSV文件填充数据库。
里克·科普兰

1
-1:过度杀毒,词典列表对此应用程序可能更有用。
S.Lott

11
字典列表使查询,排序和聚合变得更加冗长,即使使用列表推导和sort(...,key =)东西也是如此。所以我想说,在这里,sqlite内存是一个完美的选择。致每个人……
里克·科普兰2009年

2
我也将字典列表选项添加到了答案中。
里克·科普兰


20

我个人将使用行列表列表。因为每一行的数据始终是相同的顺序,所以您只需访问每个列表中的该元素,就可以轻松地按任何列进行排序。您还可以轻松地根据每个列表中的特定列进行计数,并进行搜索。它基本上和二维数组差不多。

实际上,这里唯一的缺点是您必须知道数据的顺序,并且如果更改该顺序,则必须更改搜索/排序例程以使其匹配。

您可以做的另一件事是拥有字典列表。

rows = []
rows.append({"ID":"1", "name":"Cat", "year":"1998", "priority":"1"})

这将避免需要知道参数的顺序,因此您可以浏览列表中的每个“年份”字段。


7
+1:字典列表工作得很好,并且与读写JSON或CSV文件兼容。
S.Lott

6

首先,考虑到您有一个复杂的数据检索方案,您是否确定SQLite也是过大的?

最终,您将有一个临时的,非正式指定的,bug缠身的,缓慢的SQLite实现,用Greenspun的第十条规则来解释。

就是说,您说的很对,选择一个数据结构会影响搜索,排序或计数中的一个或多个,因此,如果性能至关重要且您的数据恒定,则可以考虑为不同的目的使用多个结构。

最重要的是,衡量哪些操作会更常见,并确定哪种结构最终会降低成本。


6

有一个Table类,其表的行是字典或更好的行对象的列表

在表中不直接添加行,而是具有一种方法,该方法可以更新一些查找映射,例如,如果您不按顺序添加行或id不连续,则可以更新名称,例如也可以使用idMap

class Table(object):
    def __init__(self):
        self.rows =  []# list of row objects, we assume if order of id
        self.nameMap = {} # for faster direct lookup for row by name

    def addRow(self, row):
        self.rows.append(row)
        self.nameMap[row['name']] = row

    def getRow(self, name):
        return self.nameMap[name]


table = Table()
table.addRow({'ID':1,'name':'a'})

3

我最近亲自写了一个库BD_XML

其存在的最根本原因是作为一种在XML文件和SQL数据库之间来回发送数据的方式。

它是用西班牙语编写的(如果这对编程语言很重要),但是它非常简单。

from BD_XML import Tabla

它定义了一个名为Tabla(表)的对象,可以使用一个名称来创建该对象,以标识一个pep-246兼容数据库接口的预先创建的连接对象。

Table = Tabla('Animals') 

然后,您需要使用agregar_columna(add_column)方法添加列,并且可以使用各种关键字参数:

  • campo (字段):字段名称

  • tipo (type):存储的数据类型,可以是'varchar'和'double'之类的东西,也可以是python对象的名称,如果您不希望导出到后者的数据库。

  • defecto (默认):如果添加行时没有设置列的默认值

  • 还有其他3个,但仅用于数据库处理,实际上不起作用

喜欢:

Table.agregar_columna(campo='Name', tipo='str')
Table.agregar_columna(campo='Year', tipo='date')
#declaring it date, time, datetime or timestamp is important for being able to store it as a time object and not only as a number, But you can always put it as a int if you don't care for dates
Table.agregar_columna(campo='Priority', tipo='int')

然后,使用+ =运算符添加行(如果要创建带有额外行的副本,则添加+)

Table += ('Cat', date(1998,1,1), 1)
Table += {'Year':date(1998,1,1), 'Priority':2, Name:'Fish'}
#…
#The condition for adding is that is a container accessible with either the column name or the position of the column in the table

然后,您可以生成XML并将其写入具有exportar_XML(export_XML)和escribir_XML(write_XML)的文件中:

file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'Animals.xml'))
Table.exportar_xml()
Table.escribir_xml(file)

然后使用importar_XML(import_XML)将其导入并返回文件名,并指示您正在使用文件而不是字符串文字:

Table.importar_xml(file, tipo='archivo')
#archivo means file

高级

您可以通过这种方式以SQL方式使用Tabla对象。

#UPDATE <Table> SET Name = CONCAT(Name,' ',Priority), Priority = NULL WHERE id = 2
for row in Table:
    if row['id'] == 2:
        row['Name'] += ' ' + row['Priority']
        row['Priority'] = None
print(Table)

#DELETE FROM <Table> WHERE MOD(id,2) = 0 LIMIT 1
n = 0
nmax = 1
for row in Table:
    if row['id'] % 2 == 0:
        del Table[row]
        n += 1
        if n >= nmax: break
print(Table)

本示例假定列名为“ id”,但在您的示例中可以替换为width.pos。

if row.pos == 2:

可以从以下位置下载文件:

https://bitbucket.org/WolfangT/librerias

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.