Taming Class Attributes
This is part of the [tracer series](/tag/tracer), so have a look at its [introduction](/post/tracer-tricks) first. ![pause](/media/images/pause.png) Once upon a time, we needed ordered class attributes. That is, given: ```python class A: x = 1 y = 2 z = 3 ``` We needed ``['x', 'y', 'z']``. In Python ≥ 3.6, this is easy: dictionaries are ordered. In particular, ``A``'s body is evaluated into a dictionary, which retains the evaluation order, and becomes available as ``A.__dict__``, so all we have to do is slice out some default attributes: ```python assert sys.version_info[:2] >= (3, 6) class A: x = 1 y = 2 z = 3 order = list(A.__dict__)[1:-3] # Ignore default attributes. assert order == ['x', 'y', 'z'] ``` ![pause](/media/images/pause.png) In 3.0 ≤ Python < 3.6 it gets a little harder, and requires metaclasses. If you're not familiar with the concept, a metaclass is simply the class of a class: that is, its ``__init__`` is called whenever a class of it is created, pretty much like a class's ``__init__`` is called whenever an instance of it is created. To wit: ```python >>> class M(type): ... def __init__(cls, name, bases, attrs): ... print('Creating %s' % name) ... >>> class A(metaclass=M): ... pass ... Creating A ``` However, metaclasses also have a nifty ``__prepare__`` method, which is called before the class body is evaluated, and returns the dictionary into which that body will be evaluated. So, changing it from ``dict`` to ``collections.OrderedDict`` allows us to retain the evaluation order as before: ```python assert (3, 0) <= sys.version_info[:2] < (3, 6) class Ordered(type): def __prepare__(mcs, name, bases, **kwds): return collections.OrderedDict() def __init__(cls, name, bases, attrs): cls._order = list(attrs)[2:] # Ignore default attributes. class A(metaclass=Ordered): x = 1 y = 2 z = 3 assert A._order == ['x', 'y', 'z'] ``` ![pause](/media/images/pause.png) But what about Python 2.7? Dictionaries aren't ordered, and metaclasses don't have a ``__prepare__`` method; by the time the metaclass is invoked, the class's body has already been evaluated into an unordered dictionary, and it's too late! As David Wheeler once said: everything in computer science can be solved by another level of indirection. Instead of defining ``x`` to be ``1``, let's define it to be ``Value(1)``; because even though the class's body is evaluated into an unordered dictionary, it's still *evaluated in order*, so ``Value.__init__`` is called in order - and we capture that order there. ```python class Value(object): counter = itertools.count() def __init__(self, value): self.value = value self.order = self.counter.next() class A(object): x = Value(1) y = Value(2) z = Value(3) values = [(k, v) for k, v in A.__dict__.items() if isinstance(v, Value)] order = [k for k, v in sorted(values, key=lambda (k, v): v.order)] assert order == ['x', 'y', 'z'] ``` Of course, now we'd have to access ``A.x.value`` instead of ``A.x``, and that won't do. So let's also define a metaclass or a decorator to resolve these ``Value``s back to their values once order is achieved. ```python def ordered(cls): values = [(k, v) for k, v in cls.__dict__.items() if isinstance(v, Value)] cls._order = [k for k, v in sorted(values, key=lambda k, v: v.order)] for k, v in values: setattr(cls, k, v.value) return cls @ordered class A(object): x = Value(1) y = Value(2) z = Value(3) assert A.x == 1 assert A.y == 2 assert A.z == 3 assert A._order == ['x', 'y', 'z'] ``` But frankly, wrapping every attribute with ``Value`` is quite annoying. Can we do better? ![pause](/media/images/pause.png) Let's do what we did with the [cenum decorator](/post/reclaiming-enums), and define ``ordered`` to be a callable that returns a decorator; that is, use ``@ordered()``, not ``@ordered``, so that it runs code right before the class is defined. ```python class ordered(object): def __init__(self): self.old = sys.gettrace() sys.settrace(self.tracer) def tracer(self, frame, event, arg): # Do something with frame. sys.settrace(self.old) def __call__(self, cls): # Do something with cls. return cls ``` So we create an ``ordered`` instance, and in its ``__init__`` we register ``tracer``. It is immediately invoked on the class definition, gets that class's frame before any evaluation happens, does something to it, and unregisters itself. When the class evaluation is complete, that class's object is passed to ``__call__`` for decoration, and there we do something with it and return it. That *thing to do* is simple: we need to capture the local variable definition order, which is available as ``frame.f_code.co_names``, so we keep it and then stick it in the class object. ```python class ordered(object): def __init__(self): self.old = sys.gettrace() sys.settrace(self.tracer) def tracer(self, frame, event, arg): self.order = frame.f_code.co_names[2:] # Ignore default attributes. sys.settrace(self.old) def __call__(self, cls): cls._order = self.order return cls ``` Same as before, we can make the default attributes slicing prettier by invoking ``ordered`` on an empty class and capturing its default attributes: ```python class ordered(object): default_names = None # ... def tracer(self, frame, event, arg): names = frame.f_code.co_names if self.default_names is None: self.__class__.default_names = names self.order = [name for name in names if name not in self.default_names] sys.settrace(self.old) ``` Et voilá. You can get the code on [GitHub](http://github.com/dan-gittik/ordered).