Module shelve
is great but it only save objects when they are
explicitly assigned to a shelf. This may be error prone as for
instance in:
>>> db = shelve.open("shelf.db")
>>> db["foo"] = []
>>> db["foo"].append("hello")
>>> db["foo"]
[]
Calling append
on the object does not lead to modify the stored
object because it has not been assigned to the shelf. Instead, of a
simple call to append
we should have three instructions, which is
not really convenient:
>>> l = db["foo"]
>>> l.append("hello")
>>> db["foo"] = l
I show below a simple wrapper around shelve
to deal with this
comfortably.
Using this module, the usage becomes:
>>> store = Store("shelf.db")
>>> store["foo"] = []
>>> with store.load("foo") as foo :
... foo.append("hello")
... foo.append("world!")
>>> print(" ".join(store["foo"]))
hello world!
Method load
also accepts a default argument to work like get
:
>>> with store.load("bar", []) as bar :
... bar.extend(["bye-bye", "world!"])
>>> print(" ".join(store["bar"]))
bye-bye world!
Let’s create module store
.
We first import shelve
and create a unique object to correctly
detect when no default argument is given to store.load
(using None
is not possible because we may want to use None
as a default
argument).
import shelve
Empty = object()
Then we define the context manager that takes care of writing back the object to the shelf:
- when the context manager is initialised, it records the shelf, the key and gets the object from the shelf (or the default value if provided)
- then, when we enter the context manager, the object is returned to
be assigned as the
as
name - finally, when the context manager is exited, if no exception has been raised, the object is stored back into the shelf.
Note that we have chosen that, when an exception occurs, the object is
not stored, which implements the idea that the object may be in an
inconsistent state. A with
context is thus seen as a kind of
transaction: modified objects are saved back only if nothing goes
wrong.
class StoredObject (object) :
def __init__ (self, shelf, key, default=Empty) :
self.shelf = shelf
self.key = key
if default is Empty :
self.obj = shelf[key]
else :
self.obj = shelf.get(key, default)
def __enter__ (self) :
return self.obj
def __exit__ (self, exc_type, exc_val, exc_tb) :
if exc_val is None :
self.shelf[self.key] = self.obj
Finally, we write the wrapper around a shelf:
class Store (shelve.DbfilenameShelf) :
def load (self, key, default=Empty) :
return StoredObject(self, key, default)
def __del__ (self) :
self.db.close()
DbfilenameShelf
is the class used by shelve.open
, so we use the
same one. We just add two methods: load
that returns the context
manager defined above, and __del__
to ensure that a shelf is always
correctly closed (which, surprisingly, is not made in the default
implementation).