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