Recording attribute access and method calls

January 11, 2006

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 kid Record object with the attribute name appended to the path.
  • __call__ returns a new Record 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.

Written on 11 January 2006.
« The peculiar effects of grant funding at universities
On not logging things »

Page tools: View Source.
Search:
Login: Password:

Last modified: Wed Jan 11 01:33:50 2006
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.