代码之家  ›  专栏  ›  技术社区  ›  Roee Adler

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

  •  65
  • Roee Adler  · 技术社区  · 17 年前

    我的场景如下:我有一个数据表(少数字段,少于100行),在我的程序中广泛使用。我还需要这个数据是持久的,所以我将它保存为一个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] )
    ...
    

    列列表列表(显然会有一个用于添加行等的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] 
    

    键为(行、字段)元组的字典:

    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
    ...
    

    我相信还有其他方法…然而,当涉及到我的需求(复杂的排序和计数)时,每种方法都有缺点。

    推荐的方法是什么?

    编辑:

    要澄清的是,绩效对我来说不是一个主要问题。因为表太小了,所以我相信几乎每个操作都将在毫秒范围内,这对我的应用程序来说不是一个问题。

    6 回复  |  直到 9 年前
        1
  •  74
  •   Rick Copeland    17 年前

    在内存中有一个需要查找、排序和任意聚合的“表”确实需要SQL。你说你尝试过sqlite,但是你意识到sqlite可以使用一个只在内存中的数据库吗?

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

    然后,您可以在内存中创建/删除/查询/更新具有sqlite的所有功能的表,并且在完成后不会留下任何文件。从python 2.5开始, sqlite3 是在标准库中,所以在我看来这不是“过度杀戮”。

    下面是如何创建和填充数据库的示例:

    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
  •  30
  •   user5061    12 年前

    我知道一个很古老的问题,但是…

    熊猫数据帧似乎是这里的理想选择。

    http://pandas.pydata.org/pandas-docs/version/0.13.1/generated/pandas.DataFrame.html

    从模糊中

    二维尺寸可变、潜在异构的表格数据 带有标记轴(行和列)的结构。算术运算 行标签和列标签都对齐。可以被认为是听写式的 序列对象的容器。大熊猫原始数据结构

    http://pandas.pydata.org/

        3
  •  16
  •   AlbertoPL    17 年前

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

    这里唯一的缺点是你必须知道数据的顺序,如果你改变了顺序,你必须改变你的搜索/排序程序来匹配。

    你可以做的另一件事是列出字典。

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

    这样就不需要知道参数的顺序,所以您可以查看列表中的每个“年份”字段。

        4
  •  6
  •   Anurag Uniyal    17 年前

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

    在表中不直接添加行,而是有一个更新少数查找映射的方法,例如,对于名称 如果没有按顺序添加行或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'})
    
        5
  •  5
  •   Vinko Vrsalovic    17 年前

    首先,考虑到您有一个复杂的数据检索场景,您确定甚至连sqlite都是杀伤力过度的吗?

    最后,您将得到一个特别的、非正式指定的、bug重重的、半个sqlite的缓慢实现、改写 Greenspun's Tenth Rule .

    也就是说,您说选择一个数据结构将影响一个或多个搜索、排序或计数,这是非常正确的,因此,如果性能是最重要的,并且数据是恒定的,则可以考虑为不同的目的使用多个结构。

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

        6
  •  2
  •   serv-inc    10 年前

    我最近亲自写了一个lib,叫做bd_xml。

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

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

    from BD_XML import Tabla
    

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

    Table = Tabla('Animals') 
    

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

    • campo (字段):字段的名称

    • tipo (类型):存储的数据类型,可以是“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 (导出XML)和 escribir_XML (WraveXML):

    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方式使用Taba对象的方法。

    #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 row.pos作为示例。

    if row.pos == 2:
    

    文件可从以下位置下载:

    https://bitbucket.org/WolfangT/librerias