[CentralOH] My First metaclass
Mark Erbaugh
mark at microenh.com
Tue Apr 15 15:06:31 CEST 2008
I've been reading Wesley Chun's "Core Python Programming 2nd Edition"
and working on a large Python / wxPython / Postgres (psygopg2) database
application. I wanted to simplify handling of the data retrieved from
the database, which may also be updated and written back to the
database. psygopg2 is DBAPI 2 compliant which means and data from a
query is returned as a list (rows) of tuples (columns). psycopg2 uses
the 'pyformat' style for parameter passing which expects a mapping to
provide parameters.
I wanted to make it easier to use the data from a query. I wanted to
track if the user had updated a row, and which columns in that row had
been updated. I needed to allow the data to track new rows that had been
inserted and rows that needed to be deleted.
Lastly, I wanted the data structure to be able to handle related "child"
data, such as lineitems on an invoice or purchase order.
I developed a system of classes that met these needs. However, it was
was a little cumbersome to use and I found myself putting off adding new
database functionality as getting everything working was tedious.
I looked at some of the Python recipes such as SuperTuple and DBTuple,
but these didn't quite meet my needs.
Then I read more about classes in Chun's book, especially the __slots__
parameter, descriptors and metaclasses. I felt that that these might be
the key. After a day of playing with things, I have a working class
that meets my requirements. While the ancestor class uses some of these
advanced (to me) features, descendant classes are extremely simple, all
one needs to do is define the __slots__ attribute. It meets my current
needs:
1) struct-like (x.colname) named column access. Note that slots are used
so a misspelled column name will trigger an exception
2) mapped access (x['colname'])
3) automatic tracking of update, insert and delete as well as tracking
which columns have been updated
4) inclusion of additional columns to handle related data. For a parent
child relationship, this field could be a list containing instances of
another descendant class.
Here's what I came up with. Feel free to use it if appropriate, but
since this is my first foray into metaclasses, I would appreciate any
comments/criticisms.
Mark Erbaugh
==============================================
class D(object):
"""
descriptor class
When a data member is updated, this descriptor will automatically
set the class's _updated member to True (indicating that the row
has been updated) and add the column name to the _c_updated set
(indicating that the individual column has been updated).
"""
__slots__ = ('index','name')
def __init__(self, index, name):
self.index = index
self.name = name
def __get__(self, obj, typ=None):
return obj._data[self.index]
def __set__(self, obj, value):
try:
x = obj._data[self.index] <> value
except:
x = True
if x:
obj._updated = True
obj._c_updated.add(self.name)
obj._data[self.index] = value
# ---------------------------------------
class BaseMeta(type):
def __new__(cls, name, bases, dict):
if name <> 'Base':
for i,n in enumerate(dict['__slots__']):
dict[n] = D(i,n)
del i,n
return type.__new__(cls, name, bases, dict)
class Base(object):
"""
base class for 'record-like' objects designed to hold SQL query data
field names are the names passed in in the descendents __slots__
attribute
fields can be accessed by name x.name, or as a mapping x['name']
(mapping designed for dict-type parameters)
fields _deleted and _inserted can be used to track deletes and
inserts
fields _updated (any field updated) automatically maintained
method c_updated(col <str>) returns True if column has been updated
minimum for derived class:
define field names (and order) in class __slots__ attribute
example usage:
class AddressType(Base):
__slots__ = ('ctid','rsn','postal','name')
# end of class
cursor.execute('select ctid,rsn,postal,name from at')
return [AddressType(i) for i in cursor.fetchall()]
note: can define more fields than used in query
"""
__metaclass__ = BaseMeta
__slots__ = ('_updated', '_deleted', '_inserted', '_c_updated',
'_data')
def __init__(self, data, inserted=False):
"""
data is a sequence of field values in the same order
as the descendent classes __slots__ attribute.
instance's _inserted flag will be set to inserted
The sequence may be shorter than the __slots__ attribute
in which case trailing values will be None.
"""
self._data = [None] * len(self.__slots__)
self._data[:len(data)] = data
self._updated = False
self._c_updated = set()
self._deleted = False
self._inserted = inserted
def reset_update(self):
self._updated = False
self._c_updated.clear()
def __getitem__(self, x):
return self.__getattribute__(x)
def __setitem__(self, i, x):
self.__setattr__(i, x)
def c_updated(self, x):
return x in self._c_updated
# ---------------------------------------
if __name__ == '__main__':
import application.utility.db_access.se_access as DB
class Child(Base):
"""
descendent class example:
minimum requirement, define field names and order
in __slots__ attribute
"""
__slots__ = ('ctid','rsn','postal','name')
DB.logon('development','dev')
c= DB.CONN.cursor()
c.execute("select ctid,rsn,postal,name from at")
r = [Child(i) for i in c.fetchall()]
More information about the CentralOH
mailing list