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

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Costi
User
Beiträge: 545
Registriert: Donnerstag 17. August 2006, 14:21

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()








Zuletzt geändert von Costi am Sonntag 21. Dezember 2008, 18:46, insgesamt 1-mal geändert.
cp != mv
Benutzeravatar
gerold
Python-Forum Veteran
Beiträge: 5555
Registriert: Samstag 28. Februar 2004, 22:04
Wohnort: Oberhofen im Inntal (Tirol)
Kontaktdaten:

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
:-)
http://halvar.at | Kleiner Bascom AVR Kurs
Wissen hat eine wunderbare Eigenschaft: Es verdoppelt sich, wenn man es teilt.
Costi
User
Beiträge: 545
Registriert: Donnerstag 17. August 2006, 14:21

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'))


Zuletzt geändert von Costi am Montag 22. Dezember 2008, 15:20, insgesamt 2-mal geändert.
cp != mv
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

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
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

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.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
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.
Zuletzt geändert von lunar am Montag 22. Dezember 2008, 20:04, insgesamt 1-mal geändert.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

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
lunar

Möglich, da bin ich überfragt. Allerdings gilt das auch für das Standardformat von "repr()".
Costi
User
Beiträge: 545
Registriert: Donnerstag 17. August 2006, 14:21

@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

cp != mv
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

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.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

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))``).
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
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.
Benutzeravatar
birkenfeld
Python-Forum Veteran
Beiträge: 1603
Registriert: Montag 20. März 2006, 15:29
Wohnort: Die aufstrebende Universitätsstadt bei München

Zu diesem Thema möchte ich nur kurz http://svn.python.org/view/sandbox/trunk/dbm_sqlite/ einwerfen.
Dann lieber noch Vim 7 als Windows 7.

http://pythonic.pocoo.org/
Antworten