vanangamudi
vanangamudi

Reputation: 727

How to use python metaclass for the following scenario?

I want to create a configuration class with cascading feature. What do I mean by this? let say we have a configuration class like this

class BaseConfig(metaclass=ConfigMeta, ...):
   def getattr():
       return 'default values provided by the metaclass'

class Config(BaseConfig):
   class Embedding(BaseConfig, size=200):
       class WordEmbedding(Embedding):
           size = 300

when I use this in code I will access the configuration as follows,

def function(Config, blah, blah):
    word_embedding_size = Config.Embedding.Word.size
    char_embedding_size = Config.Embedding.Char.size

The last line access a property which does not exist in Embedding class 'Char'. That should invoke getattr() which should return 200 in this case. I am not familiar with metaclasses enough to make a good judgement, but I gues I need to define the __new__() of the metaclass.

does this approach makes sense or is there a better way to do it?

EDIT:

class Config(BaseConfig):
   class Embedding(BaseConfig, size=200):
       class WordEmbedding(Embedding):
           size = 300
   class Log(BaseConfig, level=logging.DEBUG):
       class PREPROCESS(Log):
           level = logging.INFO

#When I use 
log = logging.getLogger(level=Config.Log.Model.level) #level should be INFO

Upvotes: 1

Views: 872

Answers (2)

jsbueno
jsbueno

Reputation: 110311

This is a bit confuse. I am not sure if this would be the best notation to declare configurations with default parameters - it seems verbose. But yes, given the flexibility of metaclasses and magic methods in Python, it is possible for something like this to old all flexibility you need.

Just for the sake of it, I'd like to say that using nested classes as namespaces, like you are doing, is probably the only useful thing for them. (nested classes). It is common to see a lot of people that misunderstands Python OO at all trying to make use of nested classes.

So - for your problem, you need that in the final class, a __getattr__ method exists that can fetch default values for atributes. These attributes in turn are declared as keywords to nested classes - which also can have the same metaclass. Otherwise, the hierarchy of nested classes just work for you to fetch nested attributes, using the dot notation in Python.

Moreover, for each class in a nested set, one can pass in keyword parameters that are to be used as default, if the next level of nested classes is not defined. In the given example, trying to access Config.Embedding.Char.size with a non exisitng Char should return the default "size". Not that a __getattr__ in "Embedding" can return you a fake "Char" object - but that object is the one that have to yield a size attribute. So, our __getattr__ have yet to yield an object that has itself a propper __getattr__;

However, I will suggest a change to your requirements - instead of passing in the default values as keyword parameters, to have a reserved name - like _default inside which you can put your default attributes. That way, you can provide deeply nested default subtress, instead of just scalar values as well, and the implementation can possibly be simpler.

Actually - a lot simpler. By using keywords to the class as you propose, you'd actually need to have a metaclass set those default parameters in a data structure(it would be possible in either __new__ or __init__ though). But by just using the nested classes all the way, with a reserved name, a custom __getattr__ on the metac class will work. That will retrieve unexisting class attributes on the configuration classes themselves, and all one have to do, if a requested attribute does not exist, is try to retrieve the _default class I mentioned.

Thus, you can work with something like:

class ConfigMeta(type):
    def __getattr__(cls, attr):
        return cls._default

class Base(metaclass=ConfigMeta):
    pass

class Config(Base):
    class Embed(Base):
        class _default(Base):
            size = 200
        class Word(Base):
            size = 300

assert Config.Embed.Char.size == 200
assert Config.Embed.Word.size == 300

Btw - just last year I was working on a project to have configurations like this, with default values, but using a dictionary syntax - that is why I mentioned I am not sure the nested class would be a nice design. But since all the functionality can be provided by a metaclass with 3 LoC I guess this beats anything in the way.

Also, that is why I think being able to nest whole default subtrees can be useful for what you want - I've been there.

Upvotes: 1

heemayl
heemayl

Reputation: 42017

You can use a metaclass to set the attribute:

class ConfigMeta(type):
    def __new__(mt, clsn, bases, attrs):
        try:
            _ = attrs['size']
        except KeyError:
            attrs['size'] = 300
        return super().__new__(mt, clsn, bases, attrs)

Now if the class does not have the size attribute, it would be set to 300 (change this to meet your need).

Upvotes: 1

Related Questions