[Python-Dev] assymetry in descriptor behavior

David Abrahams dave@boost-consulting.com
Sun, 23 Feb 2003 19:46:25 -0500


Guido van Rossum <guido@python.org> writes:

>> I notice that Python supports this sort of dual access for reading
>> attributes and calling static functions, but getting that behavior for
>> mutable attributes seems unreasonably difficult: I need a property in
>> the metaclass *and* in the class.
>
> I disagree that it is *unreasonably* difficult.  Given that Python
> tries to do with a single notation ('.') where C++ has two notations
> ('.' and '::') to disambiguate cases

FWIW, it doesn't disambiguate anything:

    class C
    {
       static int x;
    };
    
    C c;
    int y = C::x;
    int z = c.x;

The compiler already knows which of 'C' and 'c' is a typename and
which is an object.  Maybe it's just a pointless syntax difference...

> not to mention declarations, I think it is reasonable that this
> unusual case requires a little more effort; you should be glad that
> it's possible at all. :-)

My heart is full of gladness!

>> 1. To throw out a straw-man suggestion, what about adding an
>>    additional protocol __set2__ which, if found, will be called
>>    instead of __set__ both for reading _and_ writing attributes on the
>>    class?
>
> Let me throw out this straw-man right away: I'm not excited about
> this.  You can write a metaclass that implements this generically
> though.

I considered these two options:

  option 1:
    introduce a separate metaclass for every wrapped class so that we
    have a place to stick a property object.

    option 1a:
        only do this if the user supplies a special extra template
        parameter to the class_<...> declaration

  option 2:
    Implement a special property type which allows us to easily
    identify property attributes which correspond to Boost.Python static
    data members

    Implement a special __setattr__ in the Boost.Python metaclass which
    looks up the attribute on the class to see if it has this special
    type; if so, it is called, and otherwise the default __setattr__
    behavior takes effect.

I was going to go with option 1.  Are you suggesting option 2?

>> 2. What are the optional type=None arguments for?  It seems as though
>>    only the middle argument (obj) is ever None.  I just copied this
>>    protocol out of descrintro.html
>
> Only __get__ has both obj and type as arguments; __set__ has obj and
> value, __delete__ only obj.
>
> __get__ has obj and type because it can be used for instance and class
> attribute access.  When called for a class, obj is None because it is
> unavailable; but when called for an instance, type is set to obj's
> class, for the convenience of descriptors that aren't interested in
> the instance (like staticmethod and classmethod).

Yes, but type is always non-None, it seems. Look at descrintro.html
again.  It says:

    Example: coding super in Python.

    As an illustration of the power of the new system, here's a fully
    functional implementation of the super() built-in class in pure
    Python. This may also help clarify the semantics of super() by
    spelling out the search in ample detail. The print statement at the
    bottom of the following code prints "DCBA".

    class Super(object):
        def __init__(self, type, obj=None):
            self.__type__ = type
            self.__obj__ = obj
        def __get__(self, obj, type=None):
                               ^^^^^^^^^
            if self.__obj__ is None and obj is not None:
                return Super(self.__type__, obj)
            else:
                return self

Also, PEP 252 says:

    - __get__(): a function callable with one or two arguments that
      retrieves the attribute value from an object.

What's that about?  Does __get__ ever get called with just one
argument (excluding self)?  If so, when?

>> 3. Is there documentation for __delete__ anywhere?
>
> Apparently not, but it's easy to guess what it does if you know
> __getattr__, __setattr__ and __delattr__.  It's __delete__ and not
> __del__ because __del__ is already taken.  In an early alpha release
> it was actually __del__, but that didn't work very well. :-)

It's easy enough to guess what it does by instrumenting it, too.
However, it's been out in a released Python for several versions now
and I get tired of guessing every time I have to write a new one of
these ;-).  I think it's time there were some official docs.

-- 
Dave Abrahams
Boost Consulting
www.boost-consulting.com