Unexpected behavior of read only attributes and super

Steven Bethard steven.bethard at gmail.com
Thu Dec 8 17:28:15 EST 2005


Samuel M. Smith wrote:
> If you would care to elaborate on the how the lookup differs with  
> method descriptor it would be most appreciated.

For the more authoritative guide, see:
     http://users.rcn.com/python/download/Descriptor.htm

The basic idea is that a descriptor is an object that sits at the class 
level, and redefines how some attribute accesses work.  Consider a 
simple example:

 >>> class D(object):
...     def __get__(self, obj, objtype=None):
...         if obj is None:
...             return 'called from class %r' % objtype
...         else:
...             return 'called from instance %r' % obj
...
 >>> class C(object):
... 	d = D()
...
 >>> C.d
"called from class <class '__main__.C'>"
 >>> C().d
'called from instance <__main__.C object at 0x00E73A30>'

As you can see, instances of the D class, when used as class attributes, 
can tell whether they're being called by the class or the instance. 
This means that descriptors with a __get__ method defined can do just 
about anything on an attribute access.

Note that all functions in Python are descriptors, and they use the 
__get__ method to return either an unbound method or a bound method, 
depending on whether they were called from the type or the instance:

 >>> def f(x):
...     return x*2
...
 >>> class C(object):
...     func = f
...
 >>> f
<function f at 0x00E69C30>
 >>> C.func
<unbound method C.f>
 >>> C().func
<bound method C.f of <__main__.C object at 0x00E73B50>>

> This might help explain why it is that when I define __slots__, the  
> behavior when writing an attribute is different

Yes.  Defining __slots__ basically tells the class to create descriptors 
for each name in the list.  So, for example:

>  >>> class C(dict):
> ...     __slots__ = ['a','b']
> ...

Creates two descriptors that are attributes of class C: one named "a" 
and one named "b".

> Now the behavior is different for class variables and methods when  
> slots defined versus when slots is not defined.
> 
>  >>> c.__iter__ = 4
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> AttributeError: 'C' object attribute '__iter__' is read-only

Here, Python is trying to set the "__iter__" attribute of the object. 
Since you defined __slots__, it tells you that it can't.  So it never 
even looks at the type.

>  >>> super(C,c).__iter__ = 4
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> TypeError: 'super' object has only read-only attributes (assign  to 
> .__iter__)

In this case, you explicitly request the superclass, so you get the same 
error as before because you bypass the __slots__, which are defined for 
instances of C, not for instances of the superclass, dict.

> Then why wasn't __class__ added to c.__dict__ ? Looks like namespace  
> searching to me.

No, as you conclude later, __class__ is special, so you can still assign 
to __class__ even when __slots__ is defined because it's not considered 
a normal attribute.  But note that __class__ is an *instance* attribute, 
not a class attribute, so "c.__class__ = C" changes the class of that 
single instance, and makes no change to the type:

 >>> class C(object):
...     pass
...
 >>> class D(C):
...     pass
...
 >>> c1 = C()
 >>> c2 = C()
 >>> C, c1, c2
(<class '__main__.C'>, <__main__.C object at 0x00E73A30>, <__main__.C 
object at 0x00E73210>)
 >>> c1.__class__ = D
 >>> C, c1, c2
(<class '__main__.C'>, <__main__.D object at 0x00E73A30>, <__main__.C 
object at 0x00E73210>)

So no, even with __class__, you're only assigning to the instance, and 
so Python's not searching any additional namespaces.

> now with slots defined
> 
>  >>> class C(dict):
> ...     __slots__ = ['b']
> ...     a = 0
> ...
>  >>> c = C()
>  >>> c.a
> 0
>  >>> c.a = 4
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> AttributeError: 'C' object attribute 'a' is read-only
>  >>> C.a = 5
>  >>> c.a
> 5
> 
> So the rule is that when __slots__ is defined class variables become  
> read only.

That's not quite right.  As you show above, class variables are still 
modifiable from the class object.  But yes, defining __slots__ means 
that, from an instance, you can only modify the attributes defined in 
__slots__.

> What if the class variable is included in __slots__
> 
>  >>> class C(dict):
> ...     __slots__ = ['b']
> ...     b = 1
> ...
>  >>> c = C()
>  >>> c.b
> 1
>  >>> c.b = 2
> Traceback (most recent call last):
>   File "<stdin>", line 1, in ?
> AttributeError: 'C' object attribute 'b' is read-only
> 
> So even though b is in slots I still can't create an instance  variable 
> by that name and shadow the class variable.

Yes, this behavior is documented:
     http://docs.python.org/ref/slots.html

"""
__slots__ are implemented at the class level by creating descriptors 
(3.3.2) for each variable name. As a result, class attributes cannot be 
used to set default values for instance variables defined by __slots__; 
otherwise, the class attribute would overwrite the descriptor assignment.
"""

The documentation isn't great, I'll agree, but the result is basically 
that if you combine __slots__ with class attributes of the same name, 
you're asking for trouble because your 'b' class attribute and your 'b' 
slot both reside at the class level.  IMO, the manual should probably 
explicitly say that the behavior of such a class is undefined.

> It feels like the implementation of slots is half baked.

I wouldn't go that far.  Once you understand that __slots__ reside as 
descriptors at the class level, you can see why most of the problems 
arise.  That said, I've never had the need to use __slots__ in any real 
world code.

> Because slots break this paradigm then at the very least the error  
> messages should point out that this object
> is using slots "so beware".
> 
> For example I would prefer something like the following
> 
> c.a
> AttributeError: Slot 'a' not yet assigned
> 
> c.c
> AttributeError: No slot named 'c' on instance
> 
> c.c = 4
> AttributeError: No slot named 'c' on instance
> 
> if change rule to not access class from instance when slots define
> c.__iter__ = 4
> AttributeError: No slot named '__iter__' on instance
> 
> or with current behavior
> AttributeError: No slot name '__iter__' class 'C' object attribute  
> '__iter__' is read-only
> 
> super(C,c).__iter__ = 4
> TypeError: 'super' object has only read-only attributes (assign  to 
> .__iter__)

These seem like pretty reasonable requests.  I would suggest adding them 
as a feature request:
     http://sourceforge.net/tracker/?group_id=5470&atid=355470
The one thing I don't know is how hard it is to produce these messages 
-- I don't know the C-level code for __slots__ at all.  Hopefully 
someone else will!

STeVe



More information about the Python-list mailing list