Toothpick Anemone
Toothpick Anemone

Reputation: 4644

When any why is it useful to have a descriptor's `__get__` method return another descriptor?

I had a container holding an object's methods, and possibly some descriptors. I wanted to test whether the descriptors had already been unwrapped or not by checking whether they had had a 'get' method. To my surprise, the __get__ method of a class-method returns an object which also has a __get__ method. Do you know when this behavior is useful? Does it have something to do with overriding class methods in a derived class?

import inspect

class K:    
    @classmethod
    def cm(cls, a, b, c):
        pass

def get_attr_info(attr):
    try:
        sig = inspect.signature(attr)
    except:
        sig = None

    attr_info = [
        ('id ', id(attr),),
        ('type  ', type(attr),),
        ('hasattr ', '__get__', hasattr(attr, '__get__'),),
        ('hasattr ', '__call__', hasattr(attr, '__call__'),),
        ('SIG:  ', sig,)
    ]
    return attr_info

get_label = lambda tpl: ' '.join([str(x) for x in tpl[0:-1]]).ljust(20)

kinst = K()
cm = object.__getattribute__(type(kinst), '__dict__')['cm']

try:
    for idx in range(0, 5):
        info = get_attr_info(cm)        
        print('\n' + '\n'.join([get_label(tpl) + str(tpl[-1]) for tpl in info]))
        cm = cm.__get__(kinst, type(kinst))
except AttributeError:
    print(idx)

The Output Is:

id                  44545808
type                <class 'classmethod'>
hasattr  __get__    True
hasattr  __call__   False
SIG:                None

id                  6437832
type                <class 'method'>
hasattr  __get__    True
hasattr  __call__   True
SIG:                (a, b, c)

id                  6437832
type                <class 'method'>
hasattr  __get__    True
hasattr  __call__   True
SIG:                (a, b, c)

id                  6437832
type                <class 'method'>
hasattr  __get__    True
hasattr  __call__   True
SIG:                (a, b, c)

id                  6437832
type                <class 'method'>
hasattr  __get__    True
hasattr  __call__   True
SIG:                (a, b, c)

Upvotes: 0

Views: 132

Answers (2)

abarnert
abarnert

Reputation: 365707

For the specific question of why bound methods are descriptors (it should be obvious why classmethod objects are, right?), see user's answer.


For the general question of why non-data descriptors need to be allowed: it would be more painful to write them. After all, the most obvious way to write a non-data descriptor is often to just return a function. And functions have to be non-data descriptors, or methods wouldn't work, defeating the whole reason descriptors were added to the language.

For example, consider the "pure-Python classmethod" example in the HOWTO:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

Or, more simply, consider staticmethod, which even in the builtin implementation just returns the function itself when bound.

Of course you could also implement any of these by building an object with a custom __call__ method, and sometimes that's worth doing (e.g., you could implement classmethod with partial, or with something that acts like partial, in which case there'd be no reason to add __get__)—but when a function just works, why not use a function?


For the more abstract question of whether you could possibly find a use for this feature… Well, sure. Among the things you can do are:

  • Create explicit 2.x-style unbound methods—that are then inspectable as methods, rather than as functions.
  • Create "rebindable" methods as part of building a prototype object system. (Normal method objects just ignore their arguments in __get__, except for checking that either the second one is a type, or the second is None and the first is a type…)
  • Create an object that mimics a function (rather than just a callable, the way methods, partials, etc. do), including being able to act as an unbound method, the result of a staticmethod, etc.

None of these are things you're going to want to do very often, but they're also not things Python has any reason to prevent you from doing.

Upvotes: 0

user2357112
user2357112

Reputation: 280456

The specific behavior you're seeing used to make sense because of unbound method objects. Back in Python 2, if you did

class Foo(object):
    def useful_method(self):
        do_stuff()

class Bar(object):
    useful_method = Foo.useful_method

Foo.useful_method would evaluate to an unbound method object, something similar to but slightly different from a function. Unbound method objects needed a __get__ so Bar().useful_method would auto-bind self, just like if Foo.useful_method had evaluated to a function instead of an unbound method during the definition of Bar.

Unbound method objects and bound method objects were implemented with the same type, so bound methods shared the same __get__ method unbound methods had. For bound method objects, though, __get__ would just return the bound method object, as if no descriptor logic was involved.

Now that unbound methods no longer exist, the __get__ method of method objects is superfluous, but it hasn't been removed.

Upvotes: 1

Related Questions