[Python-Dev] PEP 447: add type.__locallookup__

Nick Coghlan ncoghlan at gmail.com
Sat Sep 14 08:30:54 CEST 2013


On 13 September 2013 22:23, Steven D'Aprano <steve at pearwood.info> wrote:
> On Fri, Sep 13, 2013 at 08:42:46PM +1000, Nick Coghlan wrote:
>> Perhaps "__getdescriptor__" would work as the method name? Yes, it can
>> technically return a non-descriptor,
>
> So technically that name is, um, what's the term... oh yes, "a lie".
>
> :-)

In this case, "__getdescriptor__" means "I am looking for a
descriptor, please don't invoke the descriptor methods or traverse the
MRO, just return the raw object", not "you *must* give me a
descriptor".

The name suggestion comes from the fact that name bindings on the
instance and names bindings on the class are *different*, in that only
the latter participate in the descriptor protocol:

>>> class A:
...     def m(self): pass
...
>>> A.m
<function A.m at 0x7fb51ad63320>
>>> a = A()
>>> a.f = A.m
>>> a.m
<bound method A.m of <__main__.A object at 0x7fb51ad60550>>
>>> a.f
<function A.m at 0x7fb51ad63320>

It's the same function object underneath, so what's going on?

The trick is that only *types* get to play the descriptor game, where
the interpreter looks for __get__, __set__ and __delete__ on the
returned object and invokes them with found. Ordinary instances don't
have that behaviour - instead, they go through type(self) to look for
descriptors, and anything they find in the instance dictionary is
returned unaltered.

This difference in how attribute lookups are handled is actually the
most fundamental difference between normal class instances and classes
themselves (which are instances of metaclasses).

>> but the *primary* purpose is to
>> customise the retrieval of objects that will be checked to see if they're
>> descriptors.
>
> If that's the case, the PEP should make that clear.

Technically, that's what "Currently object.__getattribute__ and
super.__getattribute__ peek in the __dict__ of classes on the MRO for
a class when looking for an attribute." means.

However, I agree the current wording only conveys that to the handful
of people that already know exactly when in the attribute lookup
sequence that step occurs, which is a rather niche audience :)

It's also why I like __getdescriptor__ as a name - it's based on *why*
we're doing the lookup, rather than *how* we expect it to be done.

> [Aside: the PEP states that the method shouldn't invoke descriptors.
> What's the reason for that? If I take the statement literally, doesn't
> it mean that the method mustn't use any other methods at all? Surely
> that can't be what is intended, but I'm not sure what is intended.]

It means it shouldn't invoke __get__, __set__ or __delete__ on the
returned object, since that's the responsibility of the caller.

For example, there are times when a descriptor will be retrieved to
check for the presence of a __set__ or __delete__ method, but never
actually have its methods invoked because it is shadowed in the
instance dictionary:

>>> class Shadowable:
...     def __get__(self, *args):
...         print("Shadowable.__get__ called!")
...
>>> class Enforced:
...     def __get__(self, *args):
...         print("Enforced.__get__ called!")
...     def __set__(self, *args):
...         print("Enforced.__set__ called!")
...
>>> class Example:
...     s = Shadowable()
...     e = Enforced()
...     def __getattribute__(self, attr):
...         print("Retrieving {} from class".format(attr))
...         return super().__getattribute__(attr)
...
>>> x = Example()
>>> x.s
Retrieving s from class
Shadowable.__get__ called!
>>> x.s = 1
>>> x.s
Retrieving s from class
1

This is the key line: we retrieved 's' from the class, but *didn't*
invoke the __get__ method because it was shadowed in the instance
dictionary.

It works this way because *if* the descriptor defines __set__ or
__delete__, then Python will *ignore* the instance variable:

>>> x.e
Retrieving e from class
Enforced.__get__ called!
>>> x.__dict__["e"] = 1
Retrieving __dict__ from class
>>> x.e
Retrieving e from class
Enforced.__get__ called!

So my proposed name is based on the idea that what Ronald is after
with the PEP is a hook that *only* gets invoked when the interpreter
is doing this hunt for descriptors, but *not* for ordinary attribute
lookups.

>> It *won't* be invoked when looking for ordinary attributes in
>> an instance dict, but *will* be invoked when looking on the class object.
>
> Just to be clear, if I have:
>
> instance = MyClass()
> x = instance.name
>
> and "name" is found in instance.__dict__, then this special method will
> not be invoked. But if "name" is not found in the instance dict, then
> "name" will be looked up on the class object MyClass, which may invoke
> this special method. Am I correct?

Well, that's *my* proposal. While re-reading the current PEP, I
realised my suggested change actually goes quite a bit further than
just proposing a different name: unlike the current PEP, my advice is
that the new hook should NOT be invoked for instance attribute lookups
and should *not* replace looking directly into the class dict.
Instead, it would be the descriptor lookup counterpart to __getattr__:
whereas __getattr__ only fires for lookups on instances of the class,
__getdescriptor__ would only fire for lookups on the class itself (so,
almost exactly equivalent to defining __getattr__ on the metaclass,
but without needing a custom metaclass). A class that wanted to affect
both would be able to define __getdescriptor__ for the fallback lookup
on the class and then call it from __getattr__.

This also suggests to me that __getdescriptor__ should be an implicit
static method like __new__ (it would also be possible to make it an
implicit classmethod, but an implicit staticmethod would be more
consistent with the way __new__ works, so if we're going to have
implicit magic, it may as well be *consistent* implicit magic):

>>> class C:
...     def __new__(cls):
...         print(cls)
...         return super().__new__(cls)
...
>>> C()
<class '__main__.C'>
<__main__.C object at 0x7fb51ad60c50>
>>> class D(C): pass
...
>>> D()
<class '__main__.D'>
<__main__.D object at 0x7fb51ad60c90>
>>> C.__new__
<function C.__new__ at 0x7fb51ad638c0>
>>> C().__new__
<class '__main__.C'>
<function C.__new__ at 0x7fb51ad638c0>

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia


More information about the Python-Dev mailing list