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
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:
Recordobject remembers the path to itself.
__getattr__returns a kid
Recordobject with the attribute name appended to the path.
__call__returns a new
Recordobject 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
Because Python doesn't distinguish between 'call attribute' and
'access attribute', all of the attributes you get back are potentially
r2 could be called later).
Sidebar: the actual code
Here's a simple implementation of the
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.