Nyan
Nyan

Reputation: 2461

How to autogenerate properties in a metaclass?

How to generate properties in python using metaclass? I have some data records which fields have some relations between them. and I would like to have each record as a type(class) and auto generate those properties and relations.

I want to specify this relation as a data structure (say dict) and auto generate classes with properties

Record 1
        Field        description
       ----------------------------
        numOfPin      no# of following pin array
        pin_array     array with numOfpin elements
        read_result   if opt_flag bit 0 is set, invalid. ignore this value
        opt_flag       ...
Record 2
   ....

Edit: to clarify.. The logic for properties for missing/invalid field and optional flag is the same for all records. So I'd like to abstract this in meta-class.

e.g in pseudo python code:

record1_spec = { 'numOfpin', ('pin_array','numOfPin'),
                 'opt_flag', ('read_result','opt_flag') }
record2_spec = { 'numOfsite', ('site_array','numOfsite'),
                 'opt_flag', ('test_result','opt_flag') }

class MetaClass:
    getter_generator( ele, opt ):
       if opt : return ele
       else return None

    get class name (eg. record1) then fetch record_spec to create class
    for ele in record_spec:
       if ele is tuple type:   # relation between field
           class.attribute[ele[0]] = getter_generator(ele[0], ele[1])


class record1(metaclass = Metaclass):
    ...

Then record1 class will have all properties defined in record1_spec. The logic is if some option field is set, some properties return appropriate value.

Upvotes: 2

Views: 1198

Answers (1)

skyking
skyking

Reputation: 14395

OK, let's see how deep the rabbit hole goes...

There are two things you seem to need to do. One is to create properties on the fly, the other is to have them added. First let's look into the creation of properties, which is quite straight forward - a property is created by putting @property before the function definition, or as decorators happens to behave - to call property with the function as argument. So we create a function on the fly, and call property on that, easy:

def make_property(params):
    def fn(self):
         return use_self_and_params_to_calculate_result(self, params)
    return property(fn)

for example params could for example be a field name, and then you can return it by just do return getattr(self, fieldname) in the inner (ie fn) function.

Now for the next part, creating class. The metaclass parameter in class creation is used directly only for that - it doesn't have to be the actual metaclass, it's just called and is supposed to return something which is taken to be the class. Normal usecase though is to have the metaclass parameter being the type of the class, but we don't actually need that in this case:

def meta(name, bases, attrs):
    attrs = dict(attrs)

    attrs["x"] = make_property("_x")

    return type(name, bases, attrs)

class record1(metaclass=meta):
    pass

Now we could lookup record1_spec in meta via globals() (meta is called with "record1" as the first argument), but what's the fun in that. Let's dig deeper into the rabit hole, the meta is just called with whatever you put where baseclasses goes - it just happens to be that type expect them to be classes, but we need not do that, we only have to make sure that's true when we call type:

def meta(name, bases, attrs):
    record_spec = bases[0]
    bases = bases[1:]

    attrs = dict(attrs)

    examine_record_spec_and_populate_attrs(attrs, record_spec)
    attrs["spec"] = record_spec

    return type(name, bases, attrs)

class record1(['numOfpin', ('pin_array','numOfPin'), 
              'opt_flag', ('read_result','opt_flag')], metaclass=meta):
    pass

Then you can find the spec as record1.spec if you should need that. If your record spec is in key-value pairs (ie like a dict) you could even use keyword parameters in the class definition:

 def meta(name, bases, attrs, **spec):
    attrs = dict(attrs)

    examine_record_spec_and_populate_attrs(attrs, spec)
    attrs["spec"] = spec

    return type(name, bases, attrs)

class record1(numOfpin = ('pin_array','numOfPin'), 
              opt_flag = ('read_result','opt_flag'), metaclass=meta):
    pass

Upvotes: 5

Related Questions