[Python-Dev] Change in evaluation order in new object model

Tim Peters tim.one@home.com
Fri, 2 Nov 2001 13:32:19 -0500


[Michael McLay]
> I was suprised by a change to the order of evaluation of members
> in the new object type.  I haven't found an explanation for why the
> change was made.

Read PEP 252, paying special attention to the section containing:

    When a dynamic attribute (one defined in a regular object's
    __dict__) has the same name as a static attribute (one defined
    by a meta-object in the inheritance graph rooted at the regular
    object's __class__), the static attribute has precedence if it
    is a descriptor that defines a __set__ method (see below);
    otherwise (if there is no __set__ method) the dynamic attribute
    has precedence.  In other words, for data attributes (those
    with a __set__ method), the static definition overrides the
    dynamic definition, but for other attributes, dynamic overrides
    static.

    Rationale: we can't have a simple rule like "static overrides
    dynamic" or "dynamic overrides static", because ...

> ...
> With the new slots mechanism the order has been reversed.  The
> class level dictionary is searched and then the slots are evaluated.

I should hope so!  The *point* of __slots__ (which is what you're really
talking about, not the general concept of "slots") is that the class, not
the object, is responsible for doing the attribute name->storage_address
mapping, and in intended use an object of a class with __slots__ doesn't
even have a __dict__ (each __slot__ attribute is allocated at a fixed offset
from the start of the object, saving tons of storage).

>>> class C(object):
...     __slots__  = ['a']

Now objects of type C don't have a dict:  storage for one attribute 'a' is
allocated directly in C objects.

>>> c = C()
>>> c.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'C' object has no attribute '__dict__'

You can set and get 'a':

>>> c.a = 12
>>> c.a
12

C.a is a special beast:

>>> C.a
<member 'a' of 'C' objects>

What makes it special isn't that it came from __slots__, though, but that it
has a __set__ method (reread the quoted text above until your eyes bleed
<wink>:  this is a deadly simple protocol, so simple that it can be hard to
understand at first (shades of the metaclass hook and continuations,
there)):

>>> dir(C.a)
['__class__', '__delattr__', '__doc__', '__get__', '__getattribute__',
                                         ^^^^^^^
C.a.__get__ is called when c.a is referenced.

 '__hash__', '__init__', '__name__', '__new__', '__objclass__',
 '__reduce__', '__repr__', '__set__', '__setattr__', '__str__']
                            ^^^^^^^
C.a.__set__ is called when c.a is bound or del'ed.

Objects of C type can't grow new attributes:

>>> c.b =12
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'C' object has no attribute 'b'


> >>> class B(object):
>     __slots__ = ['a','b','c']
>
> >>> b = B()
> >>> b.a = 4
> >>> b.a
> 4
> >>> B.a = 6

Here you overwrote the descriptor that allows b.a to mean something sensible
(you nuked the <member 'a' of 'B' objects> thingie that maps 'a' to its
storage address).  Now B.a is an ordinary class attribute, and remember that
b doesn't have a __dict__ (which you asked for, by using __slots__; you're
not required to use __slots__).

> >>> b.a
> 6
> >>> b.a = 8
> Traceback (most recent call last):
>   File "<pyshell#61>", line 1, in ?
>     b.a = 8
> AttributeError: 'B' object attribute 'a' is read-only

I agree it's an odd msg, but I'm not sure it can do better easily:  by
overwriting B.a (which was nuts -- you're exploring pathologies here, not
intended usage), you've left b as an object with an 'a' attribute inherited
from its class, but also as an object that can't grow new attributes of its
own.  Python looks at "hmm, I *can't* set 'a', but I do *have* an 'a'", and
comes up with "read-only".

Try your example again without using __slots__ (you do *not* want __slots__
if you intend an object's namespace to be dynamic -- __slots__ announces
that you guarantee the set of object attributes is fixed at class creation
time):

>>> class B(object): pass
...
>>> b = B()
>>> b.a = 4
>>> B.a = 6
>>> b.a
4
>>> b.a = 8
>>> b.a
8
>>> B.a
6
>>>

IOW, don't use new features if you don't want new semantics, and things look
much the same.  If you want __slots__, though, there was no way to get its
effect prior to 2.2 short of writing an ExtensionClass in C.