如何在Python中设计一个类似sqlite的字典类,以不同字段作为“键”?

2 投票
1 回答
3589 浏览
提问于 2025-04-16 02:40

我有这样一个数据结构,

"ID  NAME  BIRTH     AGE    SEX"
=================================
1   Joe    01011980  30     M
2   Rose   12111986  24     F
3   Tom    31121965  35     M
4   Joe    15091990  20     M  

我想用python + sqlite来简单地存储和查询数据。我正在尝试设计一个类似字典的对象来存储和提取这些信息,同时这个数据库也能方便地与其他应用共享。(只是一个普通的数据库表供其他应用使用所以像pickle和ySerial这样的对象就不适合了。)

例如:

d = mysqlitedict.open('student_table')  
 d['1'] = ["Joe","01011980","30","M"]    
 d['2'] = ["Rose","12111986","24","F"]

这样做是合理的,因为我可以使用__setitem__()来处理这个问题,把“ID”作为键,其他部分作为这个类似字典对象的值。

问题是,如果我想用其他字段作为键,比如用“NAME”作为例子:

 d['Joe'] = ["1","01011980","30","M"] 

这就会成问题,因为一个类似字典的对象应该有键/值对,现在“ID”是键,“NAME”就不能作为覆盖的键。

那么我的问题是,我能设计我的类,让我可以这样做吗?

 d[key="NAME", "Joe"] = ["1","01011980","30","M"] 
 d[key="ID",'1'] = ["Joe","01011980","30","M"]  

 d.update(key = "ID", {'1':["Joe","01011980","30","M"]})

>>>d[key="NAME", 'Joe']
["1","Joe","01011980","30","M"]
["1","Joe","15091990","20","M"]

>>>d.has_key(key="NAME", 'Joe']
True

任何回复我都会很感激!

KC

1 个回答

3

sqlite 是一种 SQL 数据库,使用时最好还是按照 SQL 的方式来用(可以用 SQLAlchemy 或者其他工具,如果你真的坚持的话;-)。

d[key="NAME", 'Joe'] 这样的写法在 Python 中是完全不合法的,不管你怎么包装和努力尝试。给数据库连接做一个简单的类包装是很容易的,但你永远无法用这种语法来实现——像 d.fetch('Joe', key='Name') 这样的写法是可以做到的,但索引的写法和函数调用的写法是完全不同的,而且在函数调用中,命名参数必须放在位置参数之后。

如果你愿意放弃那些复杂的语法梦想,转而接受更合理的 Python 语法,并且需要帮助设计一个类来实现这种语法,当然可以随时问我(我很快就要去睡觉了,但我相信其他晚睡的人会很乐意帮忙;-)。

编辑:根据提问者的进一步说明(在评论中),看起来使用 set_key 方法是可以接受的,这样可以保持 Python 可接受的语法(不过语义上还是会有点问题,因为提问者想要一个“像字典一样”的对象,而这个对象可能有非唯一的键——在 Python 中其实是没有这种东西的……但我们可以尽量接近一下)。

所以,这里有一个初步的草图(需要 Python 2.6 或更高版本——因为我用了 collections.MutableMapping 来获取其他字典类似的方法,以及 .format 来格式化字符串;如果你只能用 2.5,使用 %-格式化字符串和 UserDict.DictMixin 也可以):

import collections
import sqlite3

class SqliteDict(collections.MutableMapping):
  @classmethod
  def create(cls, path, columns):
    conn = sqlite3.connect(path)
    conn.execute('DROP TABLE IF EXISTS SqliteDict')
    conn.execute('CREATE TABLE SqliteDict ({0})'.format(','.join(columns.split())))
    conn.commit()
    return cls(conn)

  @classmethod
  def open(cls, path):
    conn = sqlite3.connect(path)
    return cls(conn)

  def __init__(self, conn):
    # looks like for sime weird reason you want str, not unicode, when feasible, so...:
    conn.text_factory = sqlite3.OptimizedUnicode
    c = conn.cursor()
    c.execute('SELECT * FROM SqliteDict LIMIT 0')
    self.cols = [x[0] for x in c.description]
    self.conn = conn
    # start with a keyname (==column name) of `ID`
    self.set_key('ID')

  def set_key(self, key):
    self.i = self.cols.index(key)
    self.kn = key

  def __len__(self):
    c = self.conn.cursor()
    c.execute('SELECT COUNT(*) FROM SqliteDict')
    return c.fetchone()[0]

  def __iter__(self):
    c = self.conn.cursor()
    c.execute('SELECT * FROM SqliteDict')
    while True:
      result = c.fetchone()
      if result is None: break
      k = result.pop(self.i)
      return k, result

  def __getitem__(self, k):
    c = self.conn.cursor()
    # print 'doing:', 'SELECT * FROM SqliteDict WHERE {0}=?'.format(self.kn)
    # print ' with:', repr(k)
    c.execute('SELECT * FROM SqliteDict WHERE {0}=?'.format(self.kn), (k,))
    result = [list(r) for r in c.fetchall()]
    # print ' resu:', repr(result)
    for r in result: del r[self.i]
    return result

  def __contains__(self, k):
    c = self.conn.cursor()
    c.execute('SELECT * FROM SqliteDict WHERE {0}=?'.format(self.kn), (k,))
    return c.fetchone() is not None

  def __delitem__(self, k):
    c = self.conn.cursor()
    c.execute('DELETE FROM SqliteDict WHERE {0}=?'.format(self.kn), (k,))
    self.conn.commit()

  def __setitem__(self, k, v):
    r = list(v)
    r.insert(self.i, k)
    if len(r) != len(self.cols):
      raise ValueError, 'len({0}) is {1}, must be {2} instead'.format(r, len(r), len(self.cols))
    c = self.conn.cursor()
    # print 'doing:', 'REPLACE INTO SqliteDict VALUES({0})'.format(','.join(['?']*len(r)))
    # print ' with:', r
    c.execute('REPLACE INTO SqliteDict VALUES({0})'.format(','.join(['?']*len(r))), r)
    self.conn.commit()

  def close(self):
    self.conn.close()


def main():
  d = SqliteDict.create('student_table', 'ID NAME BIRTH AGE SEX')
  d['1'] = ["Joe", "01011980", "30", "M"]    
  d['2'] = ["Rose", "12111986", "24", "F"]
  print len(d), 'items in table created.'
  print d['2']
  print d['1']
  d.close()

  d = SqliteDict.open('student_table')
  d.set_key('NAME')
  print len(d), 'items in table opened.'
  print d['Joe']


if __name__ == '__main__':
  main()

这个类并不是直接实例化的(虽然通过传入一个打开的 sqlite3 连接到一个合适的 SqliteDict 表是可以的),而是通过两个类方法 create(用来创建一个新的数据库或清空一个已有的数据库)和 open 来使用,这似乎更符合提问者的需求,而不是让 __init__ 接受一个数据库文件路径和一个描述如何打开它的选项字符串,就像 gdbm 这样的模块一样——'r' 表示只读,'c' 表示创建或清空,'w' 表示读写——当然可以很容易地调整。传递给 create 的列名(以空格分隔的字符串)中必须有一个名为 ID 的列(我没有特别关注抛出“正确”的错误,因为在构建和使用这个类的实例时可能会发生很多用户错误;错误会在所有不正确的用法中发生,但不一定是用户明显能看出来的)。

一旦实例被打开(或创建),它的行为尽可能接近字典,除了所有设置的值必须是长度完全正确的列表,而返回的值是列表的列表(因为有“非唯一键”的奇怪问题)。例如,上面的代码运行时会打印

2 items in table created.
[['Rose', '12111986', '24', 'F']]
[['Joe', '01011980', '30', 'M']]
2 items in table opened.
[['1', '01011980', '30', 'M']]

这种“在 Python 中很荒谬”的行为是 d[x] = d[x] 失败——因为右边是一个列表,比如只有一个项目(这个项目是列值的列表),而赋值时绝对需要一个有四个项目的列表(列值)。这种荒谬是提问者要求的语义,只有通过大幅度改变这种荒谬的要求语义才能改变(例如,强制赋值时右边必须是一个列表的列表,并使用 executemany 代替普通的 execute)。

键的非唯一性也使得无法判断 d[x] = v,对于一个对应于某个数量 n 的表条目的键 k,是想替换其中一个(如果是的话,替换哪个?),还是替换所有条目,或者是添加一个新的条目。在上面的代码中,我采取了“添加另一个条目”的解释,但使用了 SQL 语句 REPLACE,如果 CREATE TABLE 被更改以指定某些唯一性约束,当唯一性约束被违反时,语义会从“添加条目”变为“替换条目”。

我会让你们自己玩这个代码,并思考一下 Python 映射和关系表之间的巨大语义差距,提问者急切想要弥补这个差距(显然是因为他想要“使用比 SQL 更好的语法”——我想知道他是否真的看过我推荐的 SqlAlchemy)。

我认为,最终重要的教训就是我在昨天回答的第一段中所说的内容,我自我引用一下……:

sqlite 是一种 SQL 数据库,使用时最好还是按照 SQL 的方式来用(可以用 SQLAlchemy 或者其他工具,如果你真的坚持的话;-)。

撰写回答