Reputation: 9859
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:
__init__
method only once during class creation__init__
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
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
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
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