Emma
Emma

Reputation: 1297

Python properties as instance attributes

I am trying to write a class with dynamic properties. Consider the following class with two read-only properties:

class Monster(object):
    def __init__(self,color,has_fur):
        self._color = color
        self._has_fur = has_fur

    @property
    def color(self): return self._color

    @property
    def has_fur(self): return self._has_fur

I want to generalize this so that __init__ can take an arbitrary dictionary and create read-only properties from each item in the dictionary. I could do that like this:

class Monster2(object):
    def __init__(self,traits):
        self._traits = traits

        for key,value in traits.iteritems():
            setattr(self.__class__,key,property(lambda self,key=key: self._traits[key]))

However, this has a serious drawback: every time I create a new instance of Monster, I am actually modifying the Monster class. Instead of creating properties for my new Monster instance, I am effectively adding properties to all instances of Monster. To see this:

>>> hasattr(Monster2,"height")
False
>>> hasattr(Monster2,"has_claws")
False
>>> blue_monster = Monster2({"height":4.3,"color":"blue"})
>>> hasattr(Monster2,"height")
True
>>> hasattr(Monster2,"has_claws")
False
>>> red_monster = Monster2({"color":"red","has_claws":True})
>>> hasattr(Monster2,"height")
True
>>> hasattr(Monster2,"has_claws")
True

This of course makes sense, since I explicitly added the properties as class attributes with setattr(self.__class__,key,property(lambda self,key=key: self._traits[key])). What I need here instead are properties that can be added to the instance. (i.e. "instance properties"). Unfortunately, according to everything I have read and tried, properties are always class attributes, not instance attributes. For example, this doesn't work:

class Monster3(object):
    def __init__(self,traits):
        self._traits = traits

        for key,value in traits.iteritems():
            self.__dict__[key] = property(lambda self,key=key: self._traits[key])

>>> green_monster = Monster3({"color":"green"})
>>> green_monster.color
<property object at 0x028FDAB0>

So my question is this: do "instance properties" exist? If not, what is the reason? I have been able to find lots about how properties are used in Python, but precious little about how they are implemented. If "instance properties" don't make sense, I would like to understand why.

Upvotes: 4

Views: 811

Answers (6)

kindall
kindall

Reputation: 184375

I don't necessarily recommend this (the __getattr__ solution is generally preferable) but you could write your class so that all instances made from it have their own class (well, a subclass of it). This is a quick hacky implementation of that idea:

class MyClass(object):
    def __new__(Class):
        Class = type(Class.__name__, (Class,), {})
        Class.__new__ = object.__new__   # to prevent infinite recursion
        return Class()

m1 = MyClass()
m2 = MyClass()
assert type(m1) is not type(m2)

Now you can set properties on type(self) with aplomb since each instance has its own class object.

@Claudiu's answer is the same kind of thing, just implemented with a function instead of integrated into the instance-making machinery.

Upvotes: 0

Claudiu
Claudiu

Reputation: 229561

Another way of doing what you want could be to dynamically create monster classes. e.g.

def make_monster_class(traits):
    class DynamicMonster(object):
        pass

    for key, val in traits.items():
        setattr(DynamicMonster, key, property(lambda self, val=val: val))

    return DynamicMonster()

blue_monster = make_monster_class({"height": 4.3, "color": "blue"})
red_monster = make_monster_class({"color": "red", "has_claws": True})
for check in ("height", "color", "has_claws"):
    print "blue", check, getattr(blue_monster, check, "N/A")
    print "red ", check, getattr(red_monster, check, "N/A")

Output:

blue height 4.3
red  height N/A
blue color blue
red  color red
blue has_claws N/A
red  has_claws True

Upvotes: 0

abarnert
abarnert

Reputation: 366073

So my question is this: do "instance properties" exist?

No.

If not, what is the reason?

Because properties are implemented as descriptors. And the magic of descriptors is that they do different things when found in an object's type's dictionary than when found in the object's dictionary.

I have been able to find lots about how properties are used in Python, but precious little about how they are implemented.

Read the Descriptor HowTo Guide linked above.


So, is there a way you could do this?

Well, yes, if you're willing to rethink the design a little.

For your case, all you want to do is use _traits in place of __dict__, and you're generating useless getter functions dynamically, so you could replace the whole thing with a couple of lines of code, as in Martijn Pieters's answer.

Or, if you want to redirect .foo to ._foo iff foo is in a list (or, better, set) of _traits, that's just as easy:

def __getattr__(self, name):
    if name in self._traits:
        return getattr(self, '_' + name)
    raise AttributeError

But let's say you actually had some kind of use for getter functions—each attribute actually needs some code to generate the value, which you've wrapped up in a function, and stored in _traits. In that case:

def __getattr__(self, name):
    getter = self._traits.get(name)
    if getter:
        return getter()
    raise AttributeError

Upvotes: 3

denz
denz

Reputation: 386

In case you don't need to make that properties read-only - you can just update object __dict__ with kwargs

class Monster(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

than you can make instances of that class like that:

m0 = Monster(name='X')
m1 = Monster(name='godzilla', behaviour='godzilla behaviour')

Upvotes: 0

Raymond Hettinger
Raymond Hettinger

Reputation: 226664

What I need here instead are properties that can be added to the instance.

A property() is a descriptor and those only work when stored in classes, not when stored in instances.

An easy way to achieve the effect of an instance property is do def __getattr__. That will let you control the behavior for lookups.

Upvotes: 1

Martijn Pieters
Martijn Pieters

Reputation: 1124558

No, there is no such thing as per-instance properties; like all descriptors, properties are always looked up on the class. See the descriptor HOWTO for exactly how that works.

You can implement dynamic attributes using a __getattr__ hook instead, which can check for instance attributes dynamically:

class Monster(object):
    def __init__(self, traits):
        self._traits = traits

    def __getattr__(self, name):
        if name in self._traits:
            return self._traits[name]
        raise AttributeError(name)

These attributes are not really dynamic though; you could just set these directly on the instance:

class Monster(object):
    def __init__(self, traits):
        self.__dict__.update(traits)

Upvotes: 8

Related Questions