Is there a way in python to create a class that inherits from another class/object passed by an object?

Viewed 43

I want to create a class that inherits another class and instantiates it with the object from the prev. class to have a new object from the new class ( old class attrs. and methods with new attrs. and methods).

example:

class A():
    attrs...

    methods...
  
class B(A):  
    def __init__(self, a_obj):
    ...

    A_attrs + B_attrs...
    
    A_methods + B_methods...

a = A()
# assign some values to 'a'
b = B(a)
# a and b should have the same params and behaviors

Is there a way to implement such an alternative class and use the new object?

1 Answers

It is hard to tell what exactly you are trying to achieve, but having the exact things you want to forward to a, it is easy to achieve.

No need to use metaclasses, but depending on the behavior you want, the special __getattribute__ and __setattr__ methods might help.

Take in mind that as far as methods are concerned, the inheritance mechanism will already do that: any methods called in an instance of B will be forwarded to the method defined in A, unless there is an overriding implementation of it in B: in this case the overriding method have to explicitly run the method in A by using a super() call, or skip it altoghether: it is up to the implementation.

Method overriding is independent of instances. If you want them to "see" a particular instance of A passed at B object instantiation, it is just the attributes in that instance that matter.

Now, if you want instances of B to proxy over to the attributes a particular instance of A, the special methods I mentioned can do the same bridge. We can implement those in a way that if any attribute access is attempted in attribute that existis in the a instance, that one is used instead. Also, the special behavior can be implemented in a mixin class, so you are free to implement your business logic in B, and deffer all special attribute handling mechanisms to the mixin instead.


_SENTINEL = object()
        
class ProxyAttrMixins:
    def __init__(self):
        # Do nothing: just prevent the proxied class` __init__ from being run
        pass
    
    def _inner_get(self, attrname):
        bound_getattr = super().__getattribute__
        try:
            proxied = bound_getattr("proxied")
        except AttributeError:
            # No associated object to proxy to!
            # just pass an try to retrieve the attribute from `self`
            pass
        else: # no AttributeError: there is a proxied object        
            associated_attr = getattr(proxied, attrname, _SENTINEL)
            if associated_attr is not _SENTINEL:
                # if object is a callable: it is a method. A mehtod in the derived class should
                # be called if it exists, and just otherwise in the proxied object:
                if callable(associated_attr):
                    try:
                        own_method = bound_getattr(attrname)
                    except AttributeError: 
                        pass
                    else:
                        return "own", own_method
                return "proxy", associated_attr
        # if there is no proxied object, or if the proxied does not have the desired attribute,
        # return the regular instance attribute:
        return "own", bound_getattr(attrname)
        
        
    def __getattribute__(self, attrname):
        bound_getattr = super().__getattribute__
        whose, attr = bound_getattr("_inner_get")(attrname)
        return attr
    
    def __setattr__(self, attrname, value):
        bound_getattr = super().__getattribute__
        try:
            whose, attr = bound_getattr("_inner_get")(attrname)
        except AttributeError:
            whose = "own"
        if whose !=  "own":
            proxied = bound_getattr("proxied")
            return setattr(proxied, attrname, value)
        super().__setattr__(attrname, value)
        
class A:
    def __init__(self, c: int):
        self.c = c
    
class B(ProxyAttrMixins, A):
    def __init__(self, a: A):
        self.proxied = a
        super().__init__()  # this ensure B can still have colaborative inheritance. 
                            # The Mixin's __init__ prevents A __init__ from being run and
                            # report on the missing `c` argument

And this code allows this kind of scenario:

In [18]: b = B(a:=A(5))

In [19]: b.c
Out[19]: 5

In [20]: b.c = 10

In [21]: a.c
Out[21]: 10
Related