Как я могу создать класс dict, подобный sqlite, в python, который может использовать другое поле в качестве ключа?

У меня есть такая структура данных,

"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 для простого хранения и запроса данных. Я пытаюсь создать объект, подобный диктовке, для хранения и извлечения этой информации, а также базу данных можно легко поделиться с другим приложением. (just a plain database table for other application, then the pickle and ySerial like object should not fit for it.)

Например:

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

Это может быть разумно, потому что я могу использовать __setitem__(), чтобы избавиться от этого, если "ID" в качестве ключа, а остальная часть - как значение этого объекта, похожего на dict.

Проблема в том, что если я хочу использовать другое поле либо как ключ семантически, например, "ИМЯ":

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

Это будет проблемой, потому что объект, подобный dict, должен семантически иметь пару ключ/значение, так как теперь «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


person K. C    schedule 12.08.2010    source источник


Ответы (1)


sqlite — это база данных SQL, и она лучше всего работает, когда используется как таковая (обернутая в SQLAlchemy или что-то в этом роде, если вы действительно настаивать;-).

Такой синтаксис, как d[key="NAME", 'Joe'], является просто недопустимым для Python, независимо от того, сколько вы будете оборачивать, пыхтеть и пыхтеть. Простая оболочка класса вокруг соединения с БД проста, но она никогда не даст вам такого синтаксиса — что-то вроде d.fetch('Joe', key='Name') достаточно легко реализовать, но синтаксис индексирования сильно отличается от синтаксиса вызовов функций, и даже в последних именованных аргументах должны стоять < em>после позиционных.

Если вы готовы отказаться от своих честолюбивых мечтаний о синтаксисе в пользу разумного синтаксиса Python и вам нужна помощь в разработке класса для реализации последнего, не стесняйтесь спрашивать, конечно (я довольно скоро пойду спать, но я уверен, что другие, более поздние спящие, будут стремиться помочь ;-).

Редактировать: учитывая пояснения OP (в комментарии), похоже, что метод set_key приемлем для поддержания приемлемого для Python синтаксиса (хотя семантика, конечно, все еще будет немного отклоняться, поскольку OP хочет "словоподобный" объект, который может иметь неуникальные ключи - на самом деле такого в Python нет... но мы можем, по крайней мере, приблизить его немного).

Итак, вот самый первый набросок (требуется Python 2.6 или выше — просто потому, что я использовал collections.MutableMapping для получения других методов, подобных dict, и .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, который, кажется, лучше соответствует желаниям OP, чем альтернатива (укажите __init__ путь к файлу БД и строку параметров, описывающую, как его открыть, точно так же, как такие модули, как gdbm take -- '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']]

«Питонически абсурдное» поведение заключается в том, что d[x] = d[x] не будет терпеть неудачу, потому что правая часть представляет собой список, например. с одним элементом (который представляет собой список значений столбца), в то время как для назначения элемента абсолютно требуется список, например. четыре элемента (значения столбца). Эта абсурдность заключается в запрошенной семантике OP, и ее можно изменить только путем повторного радикального изменения такой абсурдной требуемой семантики (например, принудительное назначение элемента для списка списков в RHS и использование executemany вместо простого execute).

Неуникальность ключей также делает невозможным угадывание, предназначен ли d[x] = v для ключа k, который соответствует некоторому числу n записей таблицы, заменять один (и если да, то какой?!) или все эти записи, или вместо этого добавьте другую новую запись. В приведенном выше коде я использовал интерпретацию «добавить еще одну запись», но с оператором SQL REPLACE, который, если CREATE TABLE изменить для указания некоторых ограничений уникальности, изменит некоторую семантику с «добавить запись» на «заменить записи», если и когда ограничения уникальности в противном случае были бы нарушены.

Я позволю вам всем поиграть с этим кодом и подумать, насколько огромен семантический разрыв между сопоставлениями Python и реляционными таблицами, который OP отчаянно стремится преодолеть (очевидно, как побочный эффект его стремления «использовать более приятный синтаксис» чем SQL — интересно, просмотрел ли он SqlAlchemy, как я рекомендовал).

Я думаю, в конце концов, важным уроком является то, что я изложил в самом начале, в первом абзаце той части ответа, которую я написал вчера, и я цитирую...:

sqlite — это база данных SQL, и она лучше всего работает, когда используется как таковая (обернутая в SQLAlchemy или что-то в этом роде, если вы действительно настаивать;-).

person Alex Martelli    schedule 12.08.2010
comment
Спасибо, Алекс, я просто пытаюсь соответствовать синтаксису питонов, чтобы сделать историю простой. И я думаю, что добавление одного свойства key_field и set_key(key=NAME) в класс может иметь смысл, потому что в фоновом режиме SQL может делать такие вещи: SELECT * from table, где self.key_field = value. - person K. C; 12.08.2010