views:

69

answers:

3

I would like to define properties for classes, and be able to access them before I actually instantiate an object of that class.

I'll give some context. My application handles a library of components. Each component is mapped to a Python class. Now I want to know what configuration a component needs, before actually instancing the class.

One solution is to write something like this:

class Component:
    @classmethod
    def config(cls, name, description, default=None):
        ''' Define one configuration switch for the class. '''
        # now put this information in a class-specific dictionary

class Model1(Component):
    @classmethod
    def define_configuration(cls):
        cls.config('n', 'number of burzs to instigate')
        cls.config('skip', 'skip any barzs among the burzs', default=True)
    # ...

component_class = Model1
component_class.define_configuration()

However, it seems pretty ugly. Ideally I would like to be able to write something like the following, and still be able to put the configuration switches in a class-specific dictionary to be accessed later.

class Model1(Component):
    config('n', 'number of burz to instigate')
    config('skip', 'skip any barz in the data', default=True)

My initial solution was to write something like:

class Model1(Component):
    Model1.config('n', 'number of burz to instigate')
    Model1.config('skip', 'skip any barz in the data', default=True)

however I discovered on other questions here on SO that the class name is not defined yet when the body is executed.

What should I do?

tl;dr: How can I get a nice syntax for defining class-specific properties (before I instance an object of that class)?


Here is (for the record) the solution that was suggested (a bit elaborated). Yai! I could get exactly what I wanted. :-)

from collections import namedtuple
Config = namedtuple('Config', 'name desc default')

def config(name, description, default=None):
    ComponentMeta.tmp_config_storage.append(Config(name, description, default))

class ComponentMeta(type):
    tmp_config_storage = []
    def __init__(cls, clsname, bases, clsdict):
        for config in ComponentMeta.tmp_config_storage:
            if not 'my_config' in cls.__dict__:
                setattr(cls, 'my_config', [])
            cls.my_config.append(config)
        ComponentMeta.tmp_config_storage = []

class Component(object):
    __metaclass__ = ComponentMeta

class Model1(Component):
    config('x1', 'for model1')
    config('y1', 'for model1')

class Model2(Component):
    config('x2', 'for model2')

print 'config for Model1:', Model1.my_config
print 'config for Model2:', Model2.my_config
+1  A: 

A class name is defined once the body is executed. This happens when the file containing the class is imported. This is not the same as creation of an instance of a class.

class A(object):
    """ doc """
    config = []
    def __init__(def):
        pass
A.config.append(('n', 'number of burz to instigate'))
A.config.append(('skip', 'skip any barz in the data', default=True))
# access class specific config before object instantiation
x = A.config[0]
# instantiating your class, post configuration access
a_obj = A()

Wouldn't this approach serve your purpose? The class config can be stored in a class variable which can be modified and added before you instantiate any objects of this class.

Using class variables should serve your purpose.

pyfunc
That's pretty cool. I never knew you could do that :-)
vlad003
you don't have to call append: you can just make `config` a list literal during class creation.
aaronasterling
Yes, that's another solution. I was wondering if it could be made less verbose. (also esthetically I'd prefer the config to be in the class definition)
Andrea Censi
+1  A: 

corrected

def config(name, description, default=None):
    ComponentMeta.config_items.append((name, description, default))

class ComponentMeta(type):
    config_items = []
    def __init__(cls, clsname, bases, clsdict):
        for options in ComponentMeta.config_items:
                cls.add_config(*options)
        ComponentMeta.config_items = []

class Component(object):
    __metaclass__ = ComponentMeta
    config_items = [] # this is only for testing. you don't need it
    @classmethod
    def add_config(cls, name, description, default=None):
        #also for testing
        cls.config_items.append((name, description, default))

class Model1(Component):
    config('n', 'number of burz to instigate')
    config('skip', 'skip any barz in the data', default=True)

print Model1.config_items

This works by making config an external function that adds the items to ComponentMeta.config_instances. ComponentMeta then checks this list when creating a class and calls config_item on the items. Note that this is not thread safe (although it could me made so). Also, if a subclass of ComponentMeta fails to call super or empty ComponentMeta.config_items itself, the next created class will get the config items.

aaronasterling
Thanks for the answer. It doesn't seem to work though.I think I understand the philosophy, but I don't understand how in these lines: ''' class Model1(Component): config('n', 'number of burz to instigate') config('skip', 'skip any barz in the data', default=True)''' where two config objects are created, how do the objects get put in the class dictionary?
Andrea Censi
@Anrdea. Correct. I'm sorta embarrassed about that actually. See my most recent update which gives you exactly what you want modulo a few potential problems. See the answer for details.
aaronasterling
Thanks a lot! I see that the trick that I would never have found by myself is to put something in a temp storage area, and then reset it each time a class is defined.
Andrea Censi
@Andrea Glad I could help. Note that you might want to make `config` a class method of `ComponentMeta` (perhaps renamed to `Component` with `Component` renamed to `ComponentBase`. This makes it a little more clear that it's only to be used in the context of creating a subclass.
aaronasterling
+1  A: 

Why not something simple like, say:

def makeconfigdict(cls):
    thedict = {}
    for name, value in cls.__dict__.items():
       if isaconfig(value):
           addconfigtodict(thedict, name, value)
           delattr(cls, name)
    cls.specialdict = thedict
    return cls

@makeconfigdict
class Model1(Component):
    n = config('number of burz to instigate')
    skip = config('skip any barz in the data', default=True)
    ...

As long as the config function returns objects that an isaconfig function can recognize, and an addconfigtodict function can properly set into the special dictionary in whatever format you want, you're in clover.

If I understand your specs correctly you don't want a normal attribute like Model1.skip, right? That's why I have that delattr call in makeconfigdict (and also why I use .items() on the class's dictionary -- since the dictionary is modified during the loop, it's better to use .items(), which takes a "snapshot" list of all names and values, rather than the usual .iteritems(), which just iterates on the dictionary, thus not allowing it to be modified during the loop).

Alex Martelli
Thanks Alex for this answer. That's an alternative. Yes, I don't want also a normal attribute. I guess I'll use the other solution proposed, which is 'hacky' in other ways, but it seems more user friendly (the user might expect that Model1.skip refers to something).
Andrea Censi