Search code examples
pythoninheritancepython-descriptors

Getting the base class in a descriptor called via super()


I have a following (simplified of course) descriptor:

class d:
    def __init__(self, method):
        self.method = method

    def __get__(self, instance, owner=None):
        print(instance, owner, self.method)
        return self.method(instance)

In __get__() I want to access the class where the decorated function is defined, but the owner argument is B on both invocations of __get__() that happen in the following code:

class A:
    @d
    def f(self):
        return "A"

class B(A):
    @d
    def f(self):
        return super().f + "B"

print(B().f)

I've checked the Descriptor HowTo Guide section on calling descriptors via super() and it says that this invocation indeed passes the subclass type to the parent class __get__(). Does it suggest I may need to define __getattribute__() to get what I want, or is there a different way? I understand that the super() call doesn't just return A but a proxy for B but I feel like there should be a way to get A in the descriptor.

I'll also appreciate a clearer explanation of what is happening in my code.


Solution

  • The easier thing to do is to record, in the descriptor, the class where it is defined at class creation time.

    Since Python 3.6 that is possible due to the addition of the __set_name__ method to the descriptor protocol.

    The owner parameter received in __set_name__ is the actuall class where the descriptor is defined. It can then be set as a descriptor attribute:

    
    class d:
        def __init__(self, method):
            self.method = method
    
        def __set_name__(self, owner, name):
            self.name = name  # should be the same as self.method.__name__
            self.owner = owner
    
        def __get__(self, instance, owner=None):
            print(f"{instance=}, {owner=}, {self.owner=}, {self.method=}")
            return self.method(instance)
    

    And running this along with your example A and B:

    (env311) [gwidion@fedora tmp01]$ python script.py 
    instance=<__main__.B object at 0x7f78c6094110>, owner=<class '__main__.B'>, self.owner=<class '__main__.B'>, self.method=<function B.f at 0x7f78c607d800>
    instance=<__main__.B object at 0x7f78c6094110>, owner=<class '__main__.B'>, self.owner=<class '__main__.A'>, self.method=<function A.f at 0x7f78c607d760>
    AB
    

    Without the resort to __set_name__ the thing to do would be indeed to walk linearly through the __mro__ until it would find self, as long as it had not been shadowed by another decorator of the same kind:

        def __get__(self, instance, owner=None):
            for cls in owner.__mro__:
                if self in cls.__dict__.values():
                    defined_at = cls 
                break
            else:
                raise RuntimeError()
            print(f"{instance=}, {owner=}, {self.defined_at=}, {self.method=}")
            return self.method(instance)