Search code examples
pythonoopclasssubclassing

Attaching attributes to a class on creation


I have a bunch of classes that get persisted in a ZODB. All classes have to share some indexing features, but also all of them define a lot of properties. The properties are defined on class level with descriptors, to be able to intercept writing to them for validation and check for duplicates in the DB. For this, every class has a list of the automatically maintained properties, _properties. They also hold _indices and some other features, which I don't want to copy on my own to every single Element-class (about 10 or so).

Consider the following code:

class BaseElement(object):
    _properties = None

class ElementFlavour1(BaseElement):
    _properties = 'a', 'b', 'c'

....

class ElementFlavourN(BaseElement):
    _properties = 'e', 'f', 'g'

for item in locals():
    if issubclass(item, BaseElement):
        item._v_properties_set = set(item._properties) if item._properties else set()

So, it basically does what it should do: I need to iterate the _properties linearly, but I also want to do quick lookups, if some property is present in a class. Nevertheless, I don't want to repeat myself and specify this set for every single flavour of BaseElement explicitly.

The current solution works, but I consider it as an ugly hack and would like to get rid of it. E.g. subclasses of BaseElement in other modules wouldn't get their properties set right. Playing around with __new__ also seems to be the wrong way, as it doesn't intercept the class construction, but the instantiation. So, how to do that properly?

P.S. I'm aware of OrderedDict, but this is not what I'm looking for. I'd like to hook into the class creation process in a clean way and append arbitrary functionality there.


Solution

  • You could do this with a metaclass:

    class BaseType(type):
        def __init__(cls, name, bases, clsdict):
            super(BaseType, cls).__init__(name, bases, clsdict)
            setattr(cls,'_v_properties_set',
                    set(cls._properties) if cls._properties else set())
    
    class BaseElement(object):
        __metaclass__ = BaseType    
        _properties = None
    
    class ElementFlavour1(BaseElement):
        _properties = 'a', 'b', 'c'
    
    class ElementFlavourN(BaseElement):
        _properties = 'e', 'f', 'g'
    
    print(ElementFlavourN._v_properties_set)
    # set(['e', 'g', 'f'])
    

    You could also do this with a class decorator, but then you'd have to decorate each subclass of BaseElement individually. The metaclass is inherited, so you only have to define it once.