kkpattern
kkpattern

Reputation: 968

Python: How to create instance attribute by metaclass?

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

Answers (1)

BrenBarn
BrenBarn

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

Related Questions