[Python-Dev] Instance variable access and descriptors

Gustavo Carneiro gjcarneiro at gmail.com
Mon Jun 11 12:43:16 CEST 2007


  While you're at it, it would be nice to fix this ugly asymmetry I found in
descriptors.  It seems that descriptor's __get__ is called even when
accessed from a class rather than instance, but __set__ is only invoked from
instances, never from classes:

class Descr(object):
    def __get__(self, obj, objtype):
        print "__get__ from instance %s, type %s" % (obj, type)
        return "foo"

    def __set__(self, obj, value):
        print "__set__ on instance %s, value %s" % (obj, value)

class Foo(object):
    foo = Descr()

print Foo.foo # works

## doesn't work, goes directly to the class dict, not calling __set__
Foo.foo = 123

  Because of this problem, I may have to install properties into a class's
metaclass achieve the same effect that I expected to achieve with a simple
descriptor :-(


On 10/06/07, Aahz <aahz at pythoncraft.com> wrote:
>
> On Sun, Jun 10, 2007, Eyal Lotem wrote:
> >
> > Python, probably through the valid assumption that most attribute
> > lookups go to the class, tries to look for the attribute in the class
> > first, and in the instance, second.
> >
> > What Python currently does is quite peculiar!
> > Here's a short description o PyObject_GenericGetAttr:
> >
> > A. Python looks for a descriptor in the _entire_ mro hierarchy
> > (len(mro) class/type check and dict lookups).
> > B. If Python found a descriptor and it has both get and set functions
> > - it uses it to get the value and returns, skipping the next stage.
> > C. If Python either did not find a descriptor, or found one that has
> > no setter, it will try a lookup in the instance dict.
> > D. If Python failed to find it in the instance, it will use the
> > descriptor's getter, and if it has no getter it will use the
> > descriptor itself.
>
> Guido, Ping, and I tried working on this at the sprint for PyCon 2003.
> We were unable to find any solution that did not affect critical-path
> timing.  As other people have noted, the current semantics cannot be
> changed.  I'll also echo other people and suggest that this discusion be
> moved to python-ideas if you want to continue pushing for a change in
> semantics.
>
> I just did a Google for my notes from PyCon 2003 and it appears that I
> never sent them out (probably because they aren't particularly
> comprehensible).  Here they are for the record (from 3/25/2003):
>
> '''
> CACHE_ATTR is the name used to describe a speedup (for new-style classes
> only) in attribute lookup by caching the location of attributes in the
> MRO.  Some of the non-obvious bits of code:
>
> * If a new-style class has any classic classes in its bases, we
> can't do attribute caching (we need to weakrefs to the derived
> classes).
>
> * If searching the MRO for an attribute discovers a data descriptor (has
> tp_descr_set), that overrides any attribute that might be in the instance;
> however, the existence of tp_descr_get still permits the instance to
> override its bases (but tp_descr_get is called if there is no instance
> attribute).
>
> * We need to invalidate the cache for the updated attribute in all derived
> classes in the following cases:
>
>     * an attribute is added or deleted to the class or its base classes
>
>     * an attribute has its status changed to or from being a data
>     descriptor
>
> This file uses Python pseudocode to describe changes necessary to
> implement CACHE_ATTR at the C level.  Except for class Meta, these are
> all exact descriptions of the work being done.  Except for class Meta the
> changes go into object.c (Meta goes into typeobject.c).  The pseudocode
> looks somewhat C-like to ease the transformation.
> '''
>
> NULL = object()
>
> def getattr(inst, name):
>     isdata, where = lookup(inst.__class__, name)
>     if isdata:
>         descr = where[name]
>         if hasattr(descr, "__get__"):
>             return descr.__get__(inst)
>         else:
>             return descr
>     value = inst.__dict__.get(name, NULL)
>     if value != NULL:
>         return value
>     if where == NULL:
>         raise AttributError
>     descr = where[name]
>     if hasattr(descr, "__get__"):
>         value = descr.__get__(inst)
>     else:
>         value = descr
>     return value
>
> def setattr(inst, name, value):
>     isdata, where = lookup(inst.__class__, name)
>     if isdata:
>         descr = where[name]
>         descr.__set__(inst, value)
>         return
>     inst.__dict__[name] = value
>
> def lookup(cls, name):
>     if cls.__cache__ != NULL:
>         pair = cls.__cache__.get(name)
>     else:
>         pair = NULL
>     if pair:
>         return pair
>     else:
>         for c in cls.__mro__:
>             where = c.__dict__
>             if name in where:
>                 descr = where[name]
>                 isdata = hasattr(descr, "__set__")
>                 pair = isdata, where
>                 break
>         else:
>             pair = False, NULL
>     if cls.__cache__ != NULL:
>         cls.__cache__[name] = pair
>     return pair
>
>
> '''
> These changes go into typeobject.c; they are not a complete
> description of what happens during creation/updates, only the
> changes necessary to implement CACHE_ATTRO.
> '''
>
> from types import ClassType
>
> class Meta(type):
>     def _invalidate(cls, name):
>         if name in cls.__cache__:
>             del cls.__cache__[name]
>         for c in cls.__subclasses__():
>             if name not in c.__dict__:
>                 self._invalidate(c, name)
>     def _build_cache(cls, bases):
>         for base in bases:
>             if type(base.__class__) is ClassType:
>                 cls.__cache__ = NULL
>                 break
>         else:
>             cls.__cache__ = {}
>     def __new__ (cls, bases):
>         self._build_cache(cls, bases)
>     def __setbases__(cls, bases):
>         self._build_cache(cls, bases)
>     def __setattr__(cls, name, value):
>         if cls.__cache__ != NULL:
>             old = cls.__dict__.get(name, NULL)
>             wasdata = old != NULL and hasattr(old, "__set__")
>             isdata = value != NULL and hasattr(value, "__set__")
>             if wasdata != isdata or (old == NULL) != (value === NULL):
>                 self._invalidate(cls, name)
>         type.__setattr__(cls, name, value)
>     def __delattr__(cls, name):
>         self.__setattr__(cls, name, NULL)
> --
> Aahz (aahz at pythoncraft.com)           <*>
> http://www.pythoncraft.com/
>
> "as long as we like the same operating system, things are cool." --piranha
> _______________________________________________
> Python-Dev mailing list
> Python-Dev at python.org
> http://mail.python.org/mailman/listinfo/python-dev
> Unsubscribe:
> http://mail.python.org/mailman/options/python-dev/gjcarneiro%40gmail.com
>



-- 
Gustavo J. A. M. Carneiro
INESC Porto, Telecommunications and Multimedia Unit
"The universe is always one step beyond logic." -- Frank Herbert
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://mail.python.org/pipermail/python-dev/attachments/20070611/11b461a5/attachment.htm 


More information about the Python-Dev mailing list