Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set instance variables from metaclass

Given a metaclass, or simpler, type(), the last argument stands as the class dict, the one which is in charge with class variables. I was wondering, is there a way to set instance variables from within a metaclass?

The reason I am using a metaclass is that I need to collect some variables defined on class creation and process them. But then, the result of this processing needs to be attached to each instance of the class, not to the class itself, since the result will be a list, which will differ from one instance of the class to another.

I will provide an example so it's easier to follow me.


I have a Model class, defined as follows:

class Model(object):
    __metaclass__ = ModelType

    has_many = None

    def __init__(self, **kwargs):
        self._fields = kwargs

The has_many class variable will be filled in at class definition by whatever model someone feels the need. In the __init__(), I simply assign the keywords provided at model instantiation to instance variable _fields.

I then have the ModelType metaclass:

class ModelType(type):
    def __new__(cls, name, bases, dct):
        if 'has_many' in dct:
            dct['_restricted_fields'].append([has-many-model-name])

What I need to do here is pretty straight-forward. I check if someone defined the has_many class variable on his custom model class, then add the variable's content to a list of restricted fields.

And now comes the important part. When instantiating a model, I need _fields to be composed of both the keyword arguments used for instantiating the model and the restricted fields, as processed in the metaclass. This would be pretty easy if _fields were a class variable in the Model class, but since it is an instance variable, I don't know how can I also add the restricted fields to it.


These being said, is there any way of achieving this? Or is there a better way of handling this? (I thought of using only a metaclass and set a _restricted_fields class variable on the model, then use this variable within the class __init__() to add the restricted fields to the normal fields, but very soon the model class will be cluttered of code which, in my opinion, should rest in another part of the code).

like image 965
linkyndy Avatar asked Dec 09 '22 09:12

linkyndy


1 Answers

Using a metaclass for this is not the correct approach here. A metaclass modifies the class-creation behavior not the instance creation behavior. You should use the __init__or the __new__ function to modify instance creation behavior. Wanting to use a metaclass for such things is using a hammer instead of a screwdriver to put a screw in a wall. ;-)

I'd suggest you use __new__ to achieve what you want. From the Python docs:

__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.

class MetaModel(type):
    def __new__(cls, name, bases, attrs):
        attrs['_restricted_fields'] = [attrs.get('has_many')]
        return type.__new__(cls, name, bases, attrs)

class Model(object):
    __metaclass__ = MetaModel
    has_many = None
    def __new__(cls, *args, **kwargs):
        instance = object.__new__(cls, *args, **kwargs)
        instance.instance_var = ['spam']
        return instance

class SubModel(Model):
    has_many = True
    def __init__(self):
        # No super call here.
        self.attr = 'eggs'

s = SubModel()
assert s._restricted_fields == [True]  # Added by MetaModel
assert s.instance_var == ['spam']  # Added by Model.__new__
assert s.attr == 'eggs'  # Added by __init__

# instance_var is added per instance.
assert SubModel().instance_var is not SubModel().instance_var

The MetaModel is responsible for creating Model classes. It adds a _restricted_fields class variable to any Model class created by it (the value is a list containing the has_many class variable).

The Model class defines a default has_many class variable. It also modifies the instance creation behavior, it adds an instance_var attribute to each created instance.

The SubModel is created by a user of your code. It defines an __init__ function to modify instance creation. Note that it does not call any super-class function, this is not a necessity. The __init__ adds an attr attribute to each SubClass instance.

like image 71
siebz0r Avatar answered Dec 11 '22 11:12

siebz0r