2

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)
15
  • You can't create an instance attribute without creating an instance. Why don't you just put code in the getter that checks if the attribute exists and returns the default value if not? Commented Oct 12, 2015 at 3:26
  • you can assign default value to the attribute ahead of time, no? Commented Oct 12, 2015 at 3:29
  • @BrenBarn I once tried to create the attribute in the getter if the attribute doesn't exists. But in this way I will have to check if attribute exists every time the getter is called. So I changed to the workaround I posted in the question latter. Commented Oct 12, 2015 at 3:31
  • I'm not sure this is a good use-case for metaclass magic. Why do you want all these attributes to be properties? Is it really just so you can force the type checking? Sounds like you're fighting against python a bit here .. Commented Oct 12, 2015 at 3:32
  • @kkpattern: Why is that a big deal? Commented Oct 12, 2015 at 3:34

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Also I want to know why shouldn't one use the injecting __init__ trick?(Do NOT consider the deep copy mutable problem, we can solve this problem by a default value factory instead of default value). What's the dangerous?
@kkpattern: Because it's needlessly confusing and doesn't really gain you anything over just doing the check inside the getter. Also I wouldn't be surprised if there are edge cases where it might fail (e.g., if some subclass calls super() in __init__ and/or the class with this metaclass is not at the top of the inheritance tree).
When comes to inheritance it's indeed dangerous to inject the __init__. Now I think doing the check inside the getter or create the instance attributes with the help of a base class is an acceptable way. But still happy to learn the injecting trick. I think it's still OK to accept this answer?
@kkpattern: Sure. I only gave this answer because you said in your comment that you were curious to learn some Python "tricks"! :-) This kind of thing is good to know about to understand how things work. It can make sense to use this sort of injection to modify a class when it's created, but you have to be careful. But injecting __init__ is especially dangerous because it's basic and often overridden.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.