Rick
Rick

Reputation: 45291

How to delegate an attribute request up the MRO chain from a descriptor

If I have a parent class and a child class, I get the following behavior in Python when asking for attributes:

class Parent():
    i = 1

class Child(Parent):
    def __init__(self, j = 2):
        self.j = j

ch = Child()
print(ch.i, ch.j) # 1 2

The request for the i and j attributes go up the MRO chain as expected; i is found in the parent class attributes, j is found in the instance attributes.

Now, if I add a generic descriptor and replace the j attribute in Child, this happens:

class _Attr():
    def __init__(self, attr_name):
        self.attr_name = '_' + attr_name
    def __get__(self, instance, klass):
        return getattr(instance, self.attr_name)

class Child(Parent):
    j = _Attr('j')
    def __init__(self, j = 2):
        self._j = j

ch = Child()
print(ch.i, ch.j) # 1 2

So far, so good.

However, using the above descriptor, if we do something like this:

class Child(Parent):
    j = _Attr('j')
    i = _Attr('i')
    def __init__(self, j = 2):
        self._j = j

ch = Child()
print(ch.i, ch.j) # AttributeError: 'Ch" object has no attribute '_i'

This error occurs because of this failed attribute lookup:

return getattr(ch, '_i')

What I want is for the descriptor to "fail silently" and for the attribute lookup to continue up the MRO chain. I'm unsure how to do this.

I tried this:

class _Attr():
    def __init__(self, attr_name):
        self.attr_name = '_' + attr_name
    def __get__(self, instance, klass):
        result = getattr(instance, self.attr_name, None)
        if result == None:
            return NotImplemented
        else:
            return result

But that does not do the trick. How can I get the behavior I want? I have a feeling I need to use super() somehow in this case, but I don't know what to do with it.

Upvotes: 1

Views: 143

Answers (2)

thefourtheye
thefourtheye

Reputation: 239643

Two things.

  1. you need to store the reference to the actual attribute name in the _Attr, so that you can use that in the parent lookup.

  2. during the lookup, you can delegate the attribute fetching work to the Parent class, with super(klass, instance)

So your _Attr would look like this

class _Attr():

    def __init__(self, attr_name):
        self._original_name = attr_name
        self.attr_name = '_' + attr_name

    def __get__(self, instance, klass):
        if hasattr(instance, self.attr_name):
            return getattr(instance, self.attr_name)
        return getattr(super(klass, instance), self._original_name)

Upvotes: 1

Kozyarchuk
Kozyarchuk

Reputation: 21867

You can take care of setting _i in the init method as follows.

class Child(P):
    j = _Attr('j')
    i = _Attr('i')
    def __init__(self, j = 2):
        self._j = j
        self._i = P.i

ch = Child()
print(ch._i, ch._j) 

Upvotes: 1

Related Questions