[Tutor] question about descriptors

Steven D'Aprano steve at pearwood.info
Fri Nov 13 07:18:48 EST 2015


On Thu, Nov 12, 2015 at 12:11:19PM +0000, Albert-Jan Roskam wrote:

> > __getattr__() is only invoked as a fallback when the normal attribute lookup 
> > fails:
> 
> 
> Aha.. and "normal attributes" live in self.__dict__?

Not necessarily.

Attributes can live either in "slots" or the instance dict, or the class 
dict, or one of the superclass dicts. Some examples may help. Let's 
start with defining a hierarchy of classes, and make an instance:


class Grandparent(object):
    spam = "from the grandparent class"
    def __getattr__(self, name):
        return "%s calculated by __getattr__" % name

class Parent(Grandparent):
    eggs = "from the parent class"

class MyClass(Parent):
    cheese = "from the instance's own class"

instance = MyClass()
instance.tomato = "from the instance itself"


The attributes defined above return their value without calling 
__getattr__:

py> instance.tomato, instance.cheese, instance.eggs, instance.spam
('from the instance itself', "from the instance's own class", 
 'from the parent class', 'from the grandparent class')

but only "tomato" lives in the instance __dict__:

py> instance.__dict__
{'tomato': 'from the instance itself'}


You can check MyClass.__dict__, etc. to see the other class attributes. 
And, of course, __getattr__ is called for anything not found in those 
dicts:

py> instance.foo
'foo calculated by __getattr__'


Slots are an alternative to dict-based attributes. If you have millions 
of instances, all with a fixed number of attributes, using a dict for 
each one can waste a lot of memory. Using slots is a way of optimizing 
for memory:

class Slotted(object):
    __slots__ = ["spam", "eggs"]
    def __init__(self):
        self.spam = 1
        self.eggs = 2
    def __getattr__(self, name):
        return "%s calculated by __getattr__" % name

x = Slotted()


This works similarly to the above, except there is no instance dict at 
all:

py> x.spam
1
py> x.eggs
2
py> x.foo
'foo calculated by __getattr__'
py> x.__dict__
'__dict__ calculated by __getattr__'


To be honest, I didn't expect that last result. I expected it to return 
Slotted.__dict__. I'm not entirely sure why it didn't.


[...]
> > If you need to intercept every attribute lookup use __getattribute__():
> 
> Fantastic, thank you for the clear explanation. Do you happen to know 
> whether the __getattr__ vs. __getattribute__ distinction was (a) a 
> deliberate design decision or (b) a historic anomaly? 

A bit of both.

Originally, classes didn't support __getattribute__. Only __getattr__ 
existed (together with __setattr__ and __delattr__), and as you have 
seen, that is only called where the normal attribute lookup mechanism 
fails. That was deliberate.

But in Python 2.2, "new style" classes were added. For technical 
reasons, new-style classes need to support intercepting every attribute 
lookup (that provides the hook for descriptors to work). So 
__getattribute__ was added, but only for new-style classes.

But be warned: writing your own __getattribute__ method is tricky to get 
right, and tends to slow down your class. So it's best avoided, unless 
you really need it.




-- 
Steve


More information about the Tutor mailing list