Reputation: 968
I'm using metaclass to create property for new classes like this:
class Property(object):
def __init__(self, internal_name, type_, default_value):
self._internal_name = internal_name
self._type = type_
self._default_value = default_value
def generate_property(self):
def getter(object_):
return getattr(object_, self._internal_name)
def setter(object_, value):
if not isinstance(value, self._type):
raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
else:
setattr(object_, self._internal_name, value)
return property(getter, setter)
class AutoPropertyMeta(type):
def __new__(cls, name, bases, attributes):
for name, value in attributes.iteritems():
if isinstance(value, Property):
attributes[name] = value.generate_property()
return super(AutoPropertyMeta, cls).__new__(cls, name, bases, attributes)
In this way I can write code like this:
class SomeClassWithALotAttributes(object):
__metaclass__ = AutoPropertyMeta
attribute_a = Property("_attribute_a", int, 0)
...
attribute_z = Property("_attribute_z", float, 1.0)
instead of:
class SomeClassWithALotAttributes(object):
def __init__(self):
self._attribute_a = 0
...
self._attribute_z = 1.0
def get_attribute_a(self):
return self._attribute_a
def set_attribute_a(self, value):
if not isinstance(value, int):
raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value))
else:
self._attribute_a = value
attribute_a = property(get_attribute_a, set_attribute_a)
...
It works great, if you always set the value before get the value of an attribute, since the AutoPropertyMeta only generate the getter
and setter
method. The actual instance attribute is created when you set the value the first time. So I want to know if there is a way to create instance attribute for a class by metaclass.
Here is a workaround I'm using now, but I always wonder if there is a better way:
class Property(object):
def __init__(self, internal_name, type_, default_value):
self._internal_name = internal_name
self._type = type_
self._default_value = default_value
def generate_property(self):
def getter(object_):
return getattr(object_, self._internal_name)
def setter(object_, value):
if not isinstance(value, self._type):
raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
else:
setattr(object_, self._internal_name, value)
return property(getter, setter)
def generate_attribute(self, object_):
setattr(object_, self._internal_name, self._default_value)
class AutoPropertyMeta(type):
def __new__(cls, name, bases, attributes):
property_list = []
for name, value in attributes.iteritems():
if isinstance(value, Property):
attributes[name] = value.generate_property()
property_list.append(value)
attributes["_property_list"] = property_list
return super(AutoPropertyMeta, cls).__new__(cls, name, bases, attributes)
class AutoPropertyClass(object):
__metaclass__ = AutoPropertyMeta
def __init__(self):
for property_ in self._property_list:
property_.generate_attribute(self)
class SomeClassWithALotAttributes(AutoPropertyClass):
attribute_a = Property("_attribute_a", int, 0)
Upvotes: 2
Views: 2105
Reputation: 251548
Here's an example of what I meant about injecting a new __init__
. Please be advised this is just for fun and you shouldn't do it.
class Property(object):
def __init__(self, type_, default_value):
self._type = type_
self._default_value = default_value
def generate_property(self, name):
self._internal_name = '_' + name
def getter(object_):
return getattr(object_, self._internal_name)
def setter(object_, value):
if not isinstance(value, self._type):
raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
else:
setattr(object_, self._internal_name, value)
return property(getter, setter)
class AutoPropertyMeta(type):
def __new__(meta, name, bases, attributes):
defaults = {}
for name, value in attributes.iteritems():
if isinstance(value, Property):
attributes[name] = value.generate_property(name)
defaults[name] = value._default_value
# create __init__ to inject into the class
# our __init__ sets up our secret attributes
if '__init__' in attributes:
realInit = attributes['__init__']
# we do a deepcopy in case default is mutable
# but beware, this might not always work
def injectedInit(self, *args, **kwargs):
for name, value in defaults.iteritems():
setattr(self, '_' + name, copy.deepcopy(value))
# call the "real" __init__ that we hid with our injected one
realInit(self, *args, **kwargs)
else:
def injectedInit(self, *args, **kwargs):
for name, value in defaults.iteritems():
setattr(self, '_' + name, copy.deepcopy(value))
# inject it
attributes['__init__'] = injectedInit
return super(AutoPropertyMeta, meta).__new__(meta, name, bases, attributes)
Then:
class SomeClassWithALotAttributes(object):
__metaclass__ = AutoPropertyMeta
attribute_a = Property(int, 0)
attribute_z = Property(list, [1, 2, 3])
def __init__(self):
print("This __init__ is still called")
>>> x = SomeClassWithALotAttributes()
This __init__ is still called
>>> y = SomeClassWithALotAttributes()
This __init__ is still called
>>> x.attribute_a
0
>>> y.attribute_a
0
>>> x.attribute_a = 88
>>> x.attribute_a
88
>>> y.attribute_a
0
>>> x.attribute_z.append(88)
>>> x.attribute_z
[1, 2, 3, 88]
>>> y.attribute_z
[1, 2, 3]
>>> x.attribute_z = 88
Traceback (most recent call last):
File "<pyshell#76>", line 1, in <module>
x.attribute_z = 88
File "<pyshell#41>", line 12, in setter
raise TypeError("Expect type {0}, got {1}.".format(self._type, type(value)))
TypeError: Expect type <type 'list'>, got <type 'int'>.
The idea is to write your own __init__
that does the initialization of the secret attributes. You then inject it into the class namespace before creating the class, but store a reference to the original __init__
(if any) so you can call it when needed.
Upvotes: 1