Recording attribute access and method calls
Here's a little bit of magic, demonstrated in the Python interpreter:
>>> from record import Record >>> r = Record() >>> r2 = r.a.b('abc').c >>> print r2 a.b('abc').c >>> print r.d.e(r2, t=20).f() d.e(Record().a.b('abc').c, t = 20).f()
Rather than making function calls or giving you real attributes, the
Record
class just records what you did (and in this case, lets it be
dumped as a string). It's smart enough to know when some of the
function arguments are also recorded bits and treat them specially.
(Here it just sticks 'Record().' in front of them, since this is an
example.)
A version of this cropped up in PingingWeblogsInPython, where it's the underlying mechanism the xmlrpclib module uses to let people invoke XML/RPC procedures as if they were writing plain Python calls.
Python allows objects to customize access to themselves (covered here), so the implementation is pretty simple:
- every
Record
object remembers the path to itself. __getattr__
returns a kidRecord
object with the attribute name appended to the path.__call__
returns a newRecord
object with the function call arguments added to the path.__str__
just returns the current path.
This creates a new object for each step of the path, but because
Record
objects never change their own path they can be safely saved
and reused by outside code (eg, in the example we reuse r
as the
starting point for two different paths; if we returned the same object
all the time and just mutated its current path, this would explode
spectacularly).
Because Python doesn't distinguish between 'call attribute' and
'access attribute', all of the attributes you get back are potentially
callable (eg, r2
could be called later).
Sidebar: the actual code
Here's a simple implementation of the Record
class.
class Record(object): def __init__(self, name=''): self._n = name def __str__(self): return self._n def __getattr__(self, attr): nn = attr if self._n: nn = '%s.%s' % (self._n, attr) return self.__class__(nn) def _gr(self, x): if isinstance(x, Record): return "Record().%s" % str(x) else: return repr(x) def __call__(self, *a, **kw): al = [self._gr(x) for x in a] k = kw.keys(); k.sort() al.extend(["%s = %s" % (x, self._gr(kw[x])) for x in k]) nn = "%s(%s)" % (self._n, ", ".join(al)) return self.__class__(nn)
(As usual, some names have been shortened to make the code not take up too much space.)
In a real version, __getattr__
probably should have a cache, so
that getting '.foo
' twice gives you the same object back.
|
|