pythoninheritancedesign-patternswrapper

Override property from wrapper class


I have a class A with a property prop and a method method that uses this property:

class A:
    @property
    def prop(self) -> int:
        return 1

    def method(self) -> int:
        return self.prop * 2  # Uses self.prop

Then I have a wrapper class that tries to override this property like this:

class B:
    def __init__(self, a: A):
        self._a = a

    @property
    def prop(self) -> int:
        return 2  # Override A's property

    def __getattr__(self, attr):
        return getattr(self._a, attr)  # Delegate attribute access to A if attr not in B

When b.method() is called, I'd like the calculation to be done with B's prop, and get 4 as result. What is happening instead is that prop is resolving within A, and b.method() returns 2.

b = B(a)

b.method() # Returns 2. Wanted 4

Is there any way I can override the property prop in A, when used on A's code, when it is being wrapped by B?

EDIT: I also want the property to be overridden for other properties too. So if I have other_prop in A as follows:

class A:
    @property
    def prop(self) -> int:
        return 1

    @property
    def other_prop(self) -> int:
        return self.prop * 4  # Uses self.prop

    def method(self) -> int:
        return self.prop * 2  # Uses self.prop

I'd like b.other_prop to return 8

Any way to achieve this?


Solution

  • You can test if the attribute obtained from _a is a method and re-bind the underlying function (accessible via the __func__ attribute) to the B instance so that the method can have access to B's properties:

    from inspect import ismethod
    
    class B:
        def __getattr__(self, attr):
            value = getattr(self._a, attr)
            if ismethod(value):
                value = value.__func__.__get__(self)
            return value
    

    Demo: https://ideone.com/jLJma8

    Note that it is unnecessary for __getattr__ to test if attr in self.__dict__: because __getattr__ is called only when an attribute name is not found in __dict__ in the first place.

    EDIT: Similarly, if you need properties of _a to be able to access the B instance, you can test if the attribute obtained from _a's type is a property and call the the property's getter function with the B instance instead. So together with the method re-binding logics above, B.__getattr__ shall become:

    class B:
        def __getattr__(self, attr):
            value = getattr(type(self._a), attr, None)
            if isinstance(value, property):
                return value.fget(self)
            value = getattr(self._a, attr)
            if ismethod(value):
                value = value.__func__.__get__(self)
            return value
    

    Demo: https://ideone.com/uE3WHl