Zaar Hai
Zaar Hai

Reputation: 9859

Python metaclass default attributes

I'm trying to create metaclass in Python (2.7) that will set arguments passed to object's __init__ as object attributes.

class AttributeInitType(type):        
    def __call__(self, *args, **kwargs):
        obj = super(AttributeInitType, self).__call__(*args, **kwargs)
        for k, v in kwargs.items():
            setattr(obj, k, v)
        return obj

Usage:

class Human(object):
    __metaclass__ = AttributeInitType

    def __init__(self, height=160, age=0, eyes="brown", sex="male"):
        pass

man = Human()

Question: I want man instance to have defaults attributes set as in class's __init__. How can I do it?

Update: I've came to even better solution that:

Here is the code:

import inspect
import copy

class AttributeInitType(type):
    """Converts keyword attributes of the init to object attributes"""
    def __new__(mcs, name, bases, d):
        # Cache __init__ defaults on a class-level
        argspec = inspect.getargspec(d["__init__"])
        init_defaults = dict(zip(argspec.args[-len(argspec.defaults):], argspec.defaults))
        cls = super(AttributeInitType, mcs).__new__(mcs, name, bases, d)
        cls.__init_defaults = init_defaults
        return cls

    def __call__(mcs, *args, **kwargs):
        obj = super(AttributeInitType, mcs).__call__(*args, **kwargs)
        the_kwargs = copy.copy(obj.__class__.__init_defaults)
        the_kwargs.update(kwargs)
        for k, v in the_kwargs.items():
            # Don't override attributes set by real __init__
            if not hasattr(obj, k):
                setattr(obj, k, v)
        return obj

Upvotes: 3

Views: 1871

Answers (3)

Ella Rose
Ella Rose

Reputation: 546

I do this in my framework with *args/**kwargs, a class defaults dictionary, and a call to attribute_setter in my base objects init. I feel like this is simpler then decoration and definitely less complicated then metaclasses.

Class Base(object):
    defaults = {"default_attribute" : "default_value"}

    def __init__(self, *args, **kwargs):
        super(Base, self).__init__()
        self.attribute_setter(self.defaults.items(), *args, **kwargs)

    def attribute_setter(self, *args, **kwargs):
        if args: # also allow tuples of name/value pairs
            for attribute_name, attribute_value in args:
                setattr(self, attribute_name, attribute_value)
        [setattr(self, name, value) for name, value in kwargs.items()]
b = Base(testing=True)
# print b.testing
# True
# print b.default_attribute
# "default_value"

this combination allows the assignment of arbitrary attributes through init at runtime by specifying them as keyword arguments (or as positional argument tuples of name/value pairs).

the class defaults dictionary is used to supply default arguments instead of explicitly named keyword arguments in init's argument list. this makes the default attributes that new instances will be created with modifiable at runtime. You can "inherit" class dictionaries via dict.copy + dict.update.

Upvotes: 0

Martijn Pieters
Martijn Pieters

Reputation: 1121834

You would need to introspect the __init__ method and extract any default values from there. The getargspec function would be helpful there.

The getargspec function returns (among others) a list of argument names, and a list of default values. You can combine these to find the default argument specification of a given function, then use that information to set attributes on the object:

import inspect

class AttributeInitType(type):        
    def __call__(self, *args, **kwargs):
        obj = super(AttributeInitType, self).__call__(*args, **kwargs)
        argspec = inspect.getargspec(obj.__init__)
        defaults = dict(zip(argspec.args[-len(argspec.defaults):], argspec.defaults))
        defaults.update(kwargs)
        for key, val in defaults.items():
            setattr(obj, key, val)
        return obj

With the above metaclass you can omit any of the arguments and they'll be set on the new instance, or you can override them by passing them in explicitly:

>>> man = Human()
>>> man.age
0
>>> man.height
160
>>> Human(height=180).height
180

Upvotes: 3

Stefano Borini
Stefano Borini

Reputation: 143795

Your situation works if you pass the arguments at object creation

>>> man
<test.Human object at 0x10a71e810>
>>> dir(man)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> man=Human(height=10)
>>> dir(man)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'height']
>>> man.height
10

but it does not work with default arguments. For that, you have to specifically extract them from the __init__ function object.

An alternative is to decorate the __init__ instead.

Upvotes: 0

Related Questions