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?
3 Answers 3
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.
-
\$\begingroup\$ Doesn't this make a lookup for every set and get action. It looks like a little bit waste to me. \$\endgroup\$yasar– yasar2012年08月28日 14:33:53 +00:00Commented Aug 28, 2012 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– SingleNegationElimination2012年08月28日 14:47:25 +00:00Commented Aug 28, 2012 at 14:47
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
.
-
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'tint
s, 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– ferada2015年03月07日 22:20:18 +00:00Commented Mar 7, 2015 at 22:20
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.