PEP 447: hooking into super() attribute resolution

Hi, I've posted a new update of my proposal to add a way to override the attribute resolution proces by super(). I've rewritten the PEP and implementation based on feedback by Steve Dower. In the current edition of the proposal the hook is a method on the type, defined in a metaclass for types. A simple demo: class meta (type): def __locallookup__(cls, name): return type.__locallookup__(cls, name.upper()) class S (metaclass=meta): def m(self): return 42 def M(self): return "fortytwo" class SS (S): def m(self): return 21 def M(self): return "twentyone" o = SS() print("self", o.m()) print("super", super(SS, o).m()) The last line currently prints "super 42" and would print "super fortytwo" with my proposal. A major open issue: the __locallookup__ method could also be used for normal attribute resolution, but that probably causes issues with attribute caching (see the PEP for details). I haven't worked out yet if it is worthwhile to tweak the proposal to fix the caching issues (even though the proposal currently says that PyObject_GenericGetAttr will use the new method). Fixing the caching issue definitly would help in my primary use case by reducing code duplication, but might end up making the API unnecessarily complex. Ronald PEP: 447 Title: Hooking into super attribute resolution Version: $Revision$ Last-Modified: $Date$ Author: Ronald Oussoren <ronaldoussoren@mac.com> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 12-Jun-2013 Post-History: 2-Jul-2013, ? Abstract ======== In current python releases the attribute resolution of the `super class`_ peeks in the ``__dict__`` attribute of classes on the MRO to look for attributes. This PEP introduces a hook that classes can use to override that behavior for specific classes. Rationale ========= Peeking in the class ``__dict__`` works for regular classes, but can cause problems when a class dynamicly looks up attributes in a ``__getattribute__`` method. This new hook makes it possible to affect attribute lookup for both normal attribute lookup and lookup through the `super class`_. The superclass attribute lookup hook ==================================== Both ``super.__getattribute__`` and ``object.__getattribute__`` (or `PyObject_GenericGetAttr`_ in C code) walk an object's MRO and peek in the class' ``__dict__`` to look up attributes. A way to affect this lookup is using a method on the meta class for the type, that by default looks up the name in the class ``__dict__``. In Python code -------------- A meta type can define a method ``__locallookup__`` that is called during attribute resolution by both ``super.__getattribute__`` and ``object.__getattribute``:: class MetaType (type): def __locallookup__(cls, name): try: return cls.__dict__[name] except KeyError: raise AttributeError(name) from None The example method above is pseudo code for the implementation of this method on `type`_ (the actual implementation is in C, and doesn't suffer from the recursion problem in this example). The ``__locallookup__`` method has as its arguments a class and the name of the attribute that is looked up. It should return the value of the attribute without invoking descriptors, or raise `AttributeError`_ when the name cannot be found. In C code --------- A new slot ``tp_locallookup`` is added to the ``PyTypeObject`` struct, this slot corresponds to the ``__locallookup__`` method on `type`_. The slot has the following prototype:: PyObject* (*locallookupfunc)(PyTypeObject* cls, PyObject* name); This method should lookup *name* in the namespace of *cls*, without looking at superclasses, and should not invoke descriptors. The method returns ``NULL`` without setting an exception when the *name* cannot be found, and returns a new reference otherwise (not a borrowed reference). Usage of this hook ------------------ The new method will be defined for `type`_, and will peek in the class dictionary:: static PyObject* type_locallookup(PyTypeObject* cls, PyObject* name) { PyObject* res; if (!cls->tp_dict) { return NULL; } res = PyDict_GetItem(cls, name); Py_XINCREF(res); return res; } The new method will be used by both `PyObject_GenericGetAttr`_ and ``super.__getattribute__`` instead of peeking in a type's ``tp_dict``. Alternative proposals --------------------- ``__getattribute_super__`` .......................... An earlier version of this PEP used the following static method on classes:: def __getattribute_super__(cls, name, object, owner): pass This method performed name lookup as well as invoking descriptors and was necessarily limited to working only with ``super.__getattribute__``. Reuse ``tp_getattro`` ..................... It would be nice to avoid adding a new slot, thus keeping the API simpler and easier to understand. A comment on `Issue 18181`_ asked about reusing the ``tp_getattro`` slot, that is super could call the ``tp_getattro`` slot of all methods along the MRO. That won't work because ``tp_getattro`` will look in the instance ``__dict__`` before it tries to resolve attributes using classes in the MRO. This would mean that using ``tp_getattro`` instead of peeking the class dictionaries changes the semantics of the `super class`_. Open Issues =========== * The names of the new slot and magic method are far from settled. * Should the python method raise an exception or return a magic value (such as the `NotImplemented`_ return value used by comparison operators). The latter could be slightly faster because it doesn't have to overhead of setting up exception state, but makes it impossible to use that value as an attribute on a class. * The proposed change to `PyObject_GenericGetAttr`_ will probably cause problems with the attribute lookup cache (MCACHE): 1. That code stores borrowed references, which won't work when the hook is present. That is mostly fixable, but at the cost of possibly keeping garbage alive. 2. Caching isn't an option when a hook might execute arbitrary code (and there hence is no reason to assume that the hooks return value won't change later on). The only workaround I could find for this is to make the hook a fallback (that is, more like ``__getattr__`` than ``__getattribute__``). References ========== * `Issue 18181`_ contains a prototype implementation Copyright ========= This document has been placed in the public domain. .. _`Issue 18181`: http://bugs.python.org/issue18181 .. _`super class`: http://docs.python.org/3/library/functions.html#super .. _`NotImplemented`: http://docs.python.org/3/library/constants.html#NotImplemented .. _`PyObject_GenericGetAttr`: http://docs.python.org/3/c-api/object.html#PyObject_GenericGetAttr .. _`type`: http://docs.python.org/3/library/functions.html#type .. _`AttributeError`: http://docs.python.org/3/library/exceptions.html#AttributeError

On Mon, Jul 15, 2013 at 8:12 AM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
I would suggest adding that motivation to the PEP, since you clearly thought it was necessary to get people to consider your proposal. :-)
A major open issue: the __locallookup__ method could also be used for normal attribute resolution, but that probably causes issues with attribute caching (see the PEP for details). I haven't worked out yet if it is worthwhile to tweak the proposal to fix the caching issues (even though the proposal currently says that PyObject_GenericGetAttr will use the new method). Fixing the caching issue definitly would help in my primary use case by reducing code duplication, but might end up making the API unnecessarily complex.
Hm. It looks like the functionality you actually want to hook into is in _PyType_Lookup(). I think that in this case the PEP's acceptance will be highly correlated with your ability to produce an actual patch that (a) implements exactly the functionality you want (never mind whether it matches the exact API currently proposed), and (b) doesn't slow down classes that don't provide this hook. Other than that, I think that it's a laudable attempt at generalizing, and I hope you solve the implementation conundrum. -- --Guido van Rossum (python.org/~guido)

On 15 Jul, 2013, at 18:49, Guido van Rossum <guido@python.org> wrote:
That's right. I didn't want to get too technical, but forgot to consider who are reading this :-)
I'd only reall need the functional change to super(), but I am working on a patch that also changes _PyType_Lookup. I think I can avoid slowdown by making the tp_locallookup optional and only "punishing" those classes that use the new slot. A minor complication is that I'll have to change the interface of _PyType_Lookup, it currently returns a borrowed reference and will return a new reference. That's just careful bookkeeping though.
Other than that, I think that it's a laudable attempt at generalizing, and I hope you solve the implementation conundrum.
I was pleasantly surprised in how the changed API was cleaner and applicable to _PyType_Lookup as well. I guess that means I'm on the right path. Ronald

On Mon, Jul 15, 2013 at 9:56 AM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
Heh. :-)
Hm. That sounds scary. I'd rename it, because this is actually an important (and scary) signature change, that type checkers otherwise won't see. Don't trust that you can find all call sites (there may be some in 3rd party code even though it starts with _). You don't want to suddenly start causing leaks all over the place. FWIW I agree that the change should affect _PyType_Lookup, because it would be very surprising if super() used a different override than regular attribute lookup.
There's a Zen of Python line about that. :-) -- --Guido van Rossum (python.org/~guido)

On Mon, Jul 15, 2013 at 8:12 AM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
I would suggest adding that motivation to the PEP, since you clearly thought it was necessary to get people to consider your proposal. :-)
A major open issue: the __locallookup__ method could also be used for normal attribute resolution, but that probably causes issues with attribute caching (see the PEP for details). I haven't worked out yet if it is worthwhile to tweak the proposal to fix the caching issues (even though the proposal currently says that PyObject_GenericGetAttr will use the new method). Fixing the caching issue definitly would help in my primary use case by reducing code duplication, but might end up making the API unnecessarily complex.
Hm. It looks like the functionality you actually want to hook into is in _PyType_Lookup(). I think that in this case the PEP's acceptance will be highly correlated with your ability to produce an actual patch that (a) implements exactly the functionality you want (never mind whether it matches the exact API currently proposed), and (b) doesn't slow down classes that don't provide this hook. Other than that, I think that it's a laudable attempt at generalizing, and I hope you solve the implementation conundrum. -- --Guido van Rossum (python.org/~guido)

On 15 Jul, 2013, at 18:49, Guido van Rossum <guido@python.org> wrote:
That's right. I didn't want to get too technical, but forgot to consider who are reading this :-)
I'd only reall need the functional change to super(), but I am working on a patch that also changes _PyType_Lookup. I think I can avoid slowdown by making the tp_locallookup optional and only "punishing" those classes that use the new slot. A minor complication is that I'll have to change the interface of _PyType_Lookup, it currently returns a borrowed reference and will return a new reference. That's just careful bookkeeping though.
Other than that, I think that it's a laudable attempt at generalizing, and I hope you solve the implementation conundrum.
I was pleasantly surprised in how the changed API was cleaner and applicable to _PyType_Lookup as well. I guess that means I'm on the right path. Ronald

On Mon, Jul 15, 2013 at 9:56 AM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
Heh. :-)
Hm. That sounds scary. I'd rename it, because this is actually an important (and scary) signature change, that type checkers otherwise won't see. Don't trust that you can find all call sites (there may be some in 3rd party code even though it starts with _). You don't want to suddenly start causing leaks all over the place. FWIW I agree that the change should affect _PyType_Lookup, because it would be very surprising if super() used a different override than regular attribute lookup.
There's a Zen of Python line about that. :-) -- --Guido van Rossum (python.org/~guido)
participants (2)
-
Guido van Rossum
-
Ronald Oussoren