Seite 1 von 1

SqlDict - das bessere shelve/(dbm fuer shelve)?

Verfasst: Sonntag 21. Dezember 2008, 16:26
von Costi
verbesserungsvorschlaege wilkommen

EDIT:
irgendwas stimmt mit mein copy & paste nicht?

nochmal EDIT:
bug gefixt

Code: Alles auswählen

#!/usr/bin/env python3



import sqlite3

import pickle

import collections



class SqlDict(collections.MutableMapping):

	def __init__(self, db_path=':memory:',

					will_pickle=True,

					debug=False):

		

		if will_pickle:

			will_pickle = lambda i: not isinstance(i, (str, int, float, bool, None.__class__))

			

		self._sql_conn = conn = sqlite3.connect(db_path)

		self.will_pickle = will_pickle

		self.debug = debug

		self._execute('''

		CREATE TABLE IF NOT EXISTS dictitem

		(key BLOB,

		value BLOB,

		PRIMARY KEY(key))''')

	

	

	def __getitem__(self, key):

		fetched = self._execute('select value from dictitem where key=?', [key]).fetchone()

		if fetched is None:

			raise ValueError('key "{0}" not found'.format(key))

		return self._load(fetched[0])

		

	def __setitem__(self, key, value):

		self._execute('insert or replace into dictitem values (?, ?)', [key, value])

		

	def __delitem__(self, key):

		self[key]

		self._execute('delete from dictitem where key=?', [key])

		

	def __len__(self):

		return self._execute('select count(*) from dictitem').fetchone()[0]

		

	def __iter__(self):

		cursor = self._execute('select key from dictitem')

		while True:

			fetched = cursor.fetchone()

			if not fetched:

				break

			yield fetched[0]

			

	def close(self):

		self.sync()

		self._sql_conn.close()

	

	def sync(self):

		self._sql_conn.commit()

	

	def _load(self, arg):

		if isinstance(arg, bytes):

			return pickle.loads(arg)

		return arg

	

	def _dump(self, arg):

		if self.will_pickle and self.will_pickle(arg):

			return pickle.dumps(arg)

		return arg

	

	def _execute(self, query, args=[]):

		cursor = self._sql_conn.cursor()

		if self.will_pickle:

			args = list(map(self._dump, args))

		if self.debug:

			print('SQL executing:', query, args)

		cursor.execute(query, args)

		return cursor





if __name__ == '__main__':

	

	d = SqlDict()

	d['bla'] = range(8)

	

	import shelve

	s = shelve.Shelf(SqlDict('/tmp/test', will_pickle=False, debug=True), writeback=True)

	s['hype hyper'] = list(d.keys())

	s.update(d)

	

	s['bla'] = list(s['bla'])

	s['bla'].append('write back works!')

	print(s['bla'][-1])

	

	

	for key in s:

		print(key)

	

	

	d.close()

	s.close()









Verfasst: Sonntag 21. Dezember 2008, 17:04
von gerold
Hallo Costi!

Ich verstehe es nicht. Was soll das für einen Sinn haben? So wie ich das im Moment verstehe ist SqlDict ein langsamer und deshalb unnützer Ersatz für shelve.

mfg
Gerold
:-)

Verfasst: Sonntag 21. Dezember 2008, 17:54
von Costi
mein problem war, dass shelve (mit dumbdbm) nach abstuerzen nicht die die datenbank wieder lesen kann

ich glaube nicht das es langsamer ist:

(EDIT: dieser test ist so nicht richtig, siehe die naechsten threads)

Code: Alles auswählen

normaler dict
fertig in 0.00016188621521

SqlDict
fertig in 0.065728187561

shelve mit dumbdbm
fertig in 2.27868795395

shelve mit gdbm
fertig in 2.36116504669

hier der test script:

Code: Alles auswählen

#!/usr/bin/env python3

#import cProfile
from time import time
from sqldict import SqlDict
import shelve
import dbm

X = 500
g =  dict(zip(('key'+str(i) for i in range(X)), ['value']*X))

def test(d):
	t = time()
	
	d.update(g)
	len(d)
	for key in list(d.keys()):
		d[key]
		del d[key]
	
	print('fertig in '+ str(time()-t))
	print()
		

print('normaler dict')
test({})

print ('SqlDict')
test(SqlDict('/tmp/foobadfdfdsar'))

print ('shelve mit dumbdbm')
dbm._names = ['dbm.dumb']
test(shelve.Shelf(dbm.open('/tmp/teafdssdfadsljkla',  'n')))

print('shelve mit gdbm')
dbm._names = ['dbm.gnu']
test(shelve.open('/tmp/fooasaadfrbaz'))



Verfasst: Sonntag 21. Dezember 2008, 18:46
von sma
Ein paar Anmerkungen zu dem Code, ohne ihn je ausprobiert zu haben:

`db_path` ist eher ein `db_file` oder `db`. Damit du später nicht ein `if self.will_pickle` testen musst, würde ich für diesen Fall ein `lambda i: False` in das Attribut schreiben (als Variante des Null-Objekt-Patterns). Warum hat `_sql_conn` einen Unterstrich, die anderen Attribute aber nicht?

Damit die DB einigermaßen effizient auf Daten zugreifen kann, sollte sie einen Index benutzen können und dazu wiederum ist ein BLOB als Datentyp eher ungeeignet, denke ich mal. Muss der Key wirklich so generisch sein? Würde nicht ein String, in den man ein `repr()` speichert, ausreichen?

Du schließt niemals deinen Cursor. Muss ich außerdem __getitem__ einen KeyError statt eines ValueError werfen? Oder hat sich das bei Python 3 geändert? Das `_execute` sollte mit `*args` definiert werden, dann kannst du dir das Erzeugen der Liste für die Parameter sparen.

Bei __delitem__ suchst du zuerst nach dem Wert, bevor du ihn löscht. Prüfe doch lieber ob die Rückgabe von DELETE 1 ist.

Dinge wie die Länge würde ich tatsächlich cachen (schließlich ist der SqlDict das einzige Objekt, das auf dieser Datenbank arbeitet) und entweder komplett lokal verwalten oder aber zumindest erst bei einem __setitem__ oder __delitem__ verwerfen.

__iter__ könnte ausnutzen, dass ein Cursor aufzählbar ist:

Code: Alles auswählen

def __iter__(self):
    with self._execute(...) as cursor:
        for row in cursor:
            yield row[0]
Musst du das Ergebnis nicht noch durch `_load` schicken?

Das `list(map(...))` in `_execute` wirkt umständlich. Warum nicht `[self._dump(a) for a in args]`. Das es besser `*args` sein sollte, sagte ich schon weiter oben.

Statt `_load` und `_dump` kann man doch lieber die register-Funktionen von sqlite3 benutzen, oder? Du gehst übrigens davon aus, dass AUTOCOMMIT der Standard by sqlite3 ist. Ist das immer der Fall? Falls nein, fehlen `commit`-Aufrufe.

Stefan

Verfasst: Sonntag 21. Dezember 2008, 18:50
von Trundle
Der Test ist doch total kaputt und liefert niemals aussagekräftige Ergebnisse: die dbm-Implementierung dürfte beides mal ``dbm.dumb`` gewesen sein (weil dbm/__init__.py `_defaultmod` setzt und deshalb `_names` überhaupt nicht mehr beachtet wird), das die Werte sofort auf die Platte speichert, wenn sie geändert werden und das wird dann gegen eine Datenbank getestet, die die Werte niemals speichert, weil kein ``commit()`` ausgeführt wird. Kein Wunder kommt man damit auf solche Ergebnisse.

Ändert man den Test um, dass auch tatsächlich gdbm benutzt wird, dann ist gdbm deutlich schneller.

Verfasst: Sonntag 21. Dezember 2008, 19:13
von lunar
sma hat geschrieben:Würde nicht ein String, in den man ein `repr()` speichert, ausreichen?
Wäre es nicht sinnvoller, "hash()" zu nutzen? "repr()" ist bei Objekten, die "__repr__()" nicht überschreiben, nicht sinnvoll, da es die "id()" bzw. die Speicheradresse ausgibt, die prozessspezifisch ist.

Verfasst: Sonntag 21. Dezember 2008, 19:52
von sma
Ich glaube nicht, dass Python garantiert, dass `hash()` für jeden Start des Interpreters den selben Wert für ein Ding liefert. Damit würde man Dinge nicht wiederfinden, wenn sie einmal gespeichert wurden. Nicht gut. Soll doch die DB lieber den hash berechnen oder sonst wie einen Index aufbauen.

Stefan

Verfasst: Sonntag 21. Dezember 2008, 20:03
von lunar
Möglich, da bin ich überfragt. Allerdings gilt das auch für das Standardformat von "repr()".

Verfasst: Montag 22. Dezember 2008, 15:17
von Costi
@sma
danke fuer die tipps, ich werde den script dementsprechen verbessern

da mir flexibilitaet wichtigier als geschwindigkeit ist, wird der key wieterhinn gepickelt werden (ausser eben bei str, int, float, bool und None)

und sorry fuer die falsche "benchmark" :wink:
ich dachte sqlite3 schreibt aendrungen sofort in die datenbank (so wars doch vor py3k ?)
also hier nochmal der test mit commits nach jeder query:

Code: Alles auswählen

normaler dict
fertig in 0.000365018844604

SqlDict
fertig in 9.7190990448

shelve mit dumbdbm
fertig in 2.42650508881


Verfasst: Montag 22. Dezember 2008, 16:26
von DasIch
lunar hat geschrieben:
sma hat geschrieben:Würde nicht ein String, in den man ein `repr()` speichert, ausreichen?
Wäre es nicht sinnvoller, "hash()" zu nutzen?
Wessen TypeErrors man bei Objekten die mutable sind wie abfängt? Ich würde eher `id()` vorschlagen.

Verfasst: Montag 22. Dezember 2008, 16:36
von Trundle
Costi hat geschrieben:ich dachte sqlite3 schreibt aendrungen sofort in die datenbank (so wars doch vor py3k ?)
AFAIK war das auch vorher nicht der Fall (außer natürlich, wenn man `connect()` das richtige `isolation_level`-Argument übergibt).
DasIch hat geschrieben:Wessen TypeErrors man bei Objekten die mutable sind wie abfängt? Ich würde eher `id()` vorschlagen.
Nein, denn das ist die Speicheradresse eine Objektes, und die ist, wie lunar bereits sagte, ja nun einmal prozessspezifisch. Und man will die Daten ja wieder auslesen können, nachdem das Programm beendet wurde. Außerdem funktioniert dann beispielsweise ein Tupel oder so nicht als Schlüssel (``id((1,2)) != id((1,2))``).

Verfasst: Montag 22. Dezember 2008, 20:03
von lunar
DasIch hat geschrieben:
lunar hat geschrieben:
sma hat geschrieben:Würde nicht ein String, in den man ein `repr()` speichert, ausreichen?
Wäre es nicht sinnvoller, "hash()" zu nutzen?
Wessen TypeErrors man bei Objekten die mutable sind wie abfängt?
Diese Ausnahmen sind nicht zu Spaß an der Freude da, sondern eben genau aus dem einen Grund, dass veränderbare Objekte als Schlüssel nicht sinnvoll sind.
Ich würde eher `id()` vorschlagen.
Hättest du mal mein Posting gelesen, dann wüsstest du auch, warum id() nun überhaupt gar nicht geeignet ist.

Verfasst: Freitag 26. Dezember 2008, 11:06
von birkenfeld
Zu diesem Thema möchte ich nur kurz http://svn.python.org/view/sandbox/trunk/dbm_sqlite/ einwerfen.