2
\$\begingroup\$

I am studying descriptors. They work with class varibles out of the box, but I want them to operate on instance variables too, so I did this:

class NumberDescriptor(object):
    def __init__(self,name):
        self.name = name

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError("%s is not a number." % value)
        else:
            setattr(instance, self.name, value)

    def __get__(self, instance, owner):
        return getattr(instance, self.name)

class Human(object):
    age = NumberDescriptor("_age")

    def __init__(self,age):
        self.age = age

a = Human(12)
print a.age
b = Human("osman")
print b.age

Is this the right way to go about this?

\$\endgroup\$
2
\$\begingroup\$

That looks reasonable; but you should handle the case of when the descriptor is invoked from the class: Human.age. In that case, __get__ is invoked with None as an argument to instance. If you've no better use for it; you can just return the descriptor itself:

def __get__(self, instance, owner):
    if instance is None:
        return self
    else:
        return getattr(instance, self.name)

Another thing you can do, which is sometimes preferred, is to make the descriptor figure out its own name by inspecting the class it's attached to; this is a little more complicated, of course, but has the advantage of allowing you to specify the "name" of the descriptor attribute just one time.

class NumberDescriptor(object):
    def snoop_name(self, owner):
        for attr in dir(owner):
            if getattr(owner, attr) is self:
                return attr
    def __get__(self, instance, owner):
        if instance is None:
            return self
        name = self.snoop_name(self, owner)
        return getattr(instance, '_'+name)
    def __set__(self, instance, value):
        name = self.snoop_name(self, type(instance))
        setattr(instance, '_' + name, int(value))

class Human(object):
    age = NumberDescriptor()

Other variations to get a similar effect (without using dir()) would be to use a metaclass that knows to look for your descriptor and sets its name there.

\$\endgroup\$
  • \$\begingroup\$ Doesn't this make a lookup for every set and get action. It looks like a little bit waste to me. \$\endgroup\$ – yasar Aug 28 '12 at 14:33
  • \$\begingroup\$ yes, this version is a little inelegant; and meant mainly to give you some ideas about how you might use descriptors. A more reasonable implementation would probably use a metaclass or a weakref.WeakKeyDict to avoid the extra lookups, but I've decided to leave that as an exercise. \$\endgroup\$ – SingleNegationElimination Aug 28 '12 at 14:47
1
\$\begingroup\$

Good question. I'm studying descriptors too.

Why do you use setattr() and getattr()? I'm not clear on why you store age inside instance. What benefit does this have over this naive example?

class NumberDescriptor(object):
    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError("%s is not a number." % value)
        else:
            self.value = value

    def __get__(self, instance, owner):
        return self.value

class Human(object):
    age = NumberDescriptor()
    worth = NumberDescriptor()

    def __init__(self,age,worth):
        self.age = age
        self.worth = worth

a = Human(12,5)
print a.age, a.worth
a.age = 13
a.worth = 6
print a.age, a.worth
b = Human("osman", 5.0)
print b.age

EDIT: responding to comment

Not sure I have a firm handle on descriptors, but the example in the docs access value with self rather than invoking getattr() and setattr().

However, this answer and this answer do not user getattr(), they do not use self either. They access the use the instance argument.

My example is slightly simpler. What are the trade-offs?

Just as in the original question, the b instance demonstrates that a ValueError is raised. I added a worth attribute simply to demonstrate that there are in fact two distinct instances of NumberDescriptor.

\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review and enjoy your stay! Your answer looks good except for the part where you construct b, as both arguments aren't ints, so maybe make the example runnable; you could also add a paragraph explaining directly why this setup is better than storing instance variables on the object itself rather than on the descriptor. \$\endgroup\$ – ferada Mar 7 '15 at 22:20
1
\$\begingroup\$

Consider this example, assume I've used your Human class and Number:

a = Human(12,5)
print a.age, a.worth
b = Human(13,5)
print b.age, b.worth

# Check if a attributes has changed:
print a.age, a.worth

Then you realize, you've overrided also 'a' instance attributes because you actually overrided class attribute :) This is why your way to do it isn't exactly right when you want to have descriptor as instance-related attribute.

When you need to have instance-related attributes which are Descriptors, you should to it in some other way, for example store in your descriptor some kind of Dict where you can identify your instance-related attributes and return then when you need them for specified instance or return some kind of default when you call attributes from class (e.g. Human.attr). It would be a good idea to use weakref.WeakKeyDictionary if you want to hold your whole instances in dict.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.