如何在Python中设计一个类似sqlite的字典类,以不同字段作为“键”?
我有这样一个数据结构,
"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 个回答
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 或者其他工具,如果你真的坚持的话;-)。