That's basic composition / delegation. The canonical approach here is to make the delegatee private (in Python: prefix it's name with a single leading underscore - it's only a convention but it's a very strong one) and write public methods to delegate operations, ie:
# naming convention: class names should be CamelCase
class Rect():
def __init__(self, x, y, w, h):
# naming convention: the single leading underscore
# denotes an implementation attribute or method
self._pos = pygame.Rect(x, y, w, h)
# explicit delegation
def move(self, x, y):
self._pos.move(x, y)
r = Rect(10,10,10,10)
r.move(10,10)
Now if you have dozens of methods to delegate, Python also provides a hook for automatic delegation: the __getattr__(self, name) magic method:
class Rect():
def __init__(self, x, y, w, h):
self._pos = pygame.Rect(x, y, w, h)
# implicit delegation:
def __getattr__(self, name):
try:
return getattr(self._pos, name)
except AttributeError:
# here we hide the fact we're delegating to another object
raise AttributeError(
"'{}'.object has no attribute '{}'".format(
type(self).__name__, name
))
r = Rect(10,10,10,10)
r.move(10,10)
The downsides of this automagic delegation are that 1/ it can provide access to some delegatee attributes you don't want to expose, 2/ delegated methods are neither explicitely visible nor discoverable thru inspection and 3/ you get some additional overhead.
First issue can be solved by keeping a whitelist of delegatee attributes you want to allow access to. There's alas no simple solution for the second point, and no solution at all for the third, so it's still better to go for explicit delegation (at least for the important parts) and keep automagic delegation for things like generic proxy classes etc.
NB: if this is a reocurring pattern in your code, you can still set up some "semi-automagic" delegation system using either a custom descriptor or a custom metaclass or a class decorator, ie (custom descriptor example):
class delegate_to():
def __init__(self, delegatee, attr=None):
self._delegatee = delegatee
self._attr = attr
self._name = None
self._owner = None
def __set_name__(self, owner, name):
self._owner = owner
self._name = name
if self._attr is None:
self._attr = name
def _raise(self):
msg = "'{}' object has no attribute '{}'".format(
self._owner.__name__,
self._attr
)
raise AttributeError(msg)
def __get__(self, obj=None, cls=None):
if obj is None:
return self
delegatee = getattr(obj, self._delegatee)
try:
return getattr(delegatee, self._attr)
except AttributeError:
self._raise(obj, cls)
def __set__(self, value):
raise AttributeError("Attribute is read-only")
def __repr__(self):
return "<Delegatee {} ({}) object for {}>".format(
self._name, self._attr, self._owner.__name__
)
# exemple use :
class Delegatee():
def __init__(self, x, y):
self.x = x
self.y = y
def move_to(self, x, y):
self.x = x
self.y = y
def move_by(self, x, y):
self.x += x
self.y += y
@property
def position(self):
return self.x, self.y
class Composite():
def __init__(self, x, y):
self._delegatee = Delegatee(x, y)
move_to = delegate_to("_delegatee")
move_by = delegate_to("_delegatee")
position = delegate_to("_delegatee")
And you can automate a bit further with a class decorator that will create the delegate_to descriptors:
class delegator():
def __init__(self, delegatee, *names):
if not names:
raise ValueError("needs one or more name to delegate")
self.delegatee = delegatee
self.names = names
def create_delegator(self, owner, name, attrname=None):
if not attrname:
attrname = name
delegator = delegate_to(self.delegatee, attrname)
delegator.__set_name__(owner, name)
return delegator
def __call__(self, cls):
for name in self.names:
if isinstance(name, tuple):
name, attr = name
else:
attr = name
delegator = self.create_delegator(name, attr)
setattr(cls, name, delegator)
return cls
@delegator("_delegatee", "move_to", "move_by", ("pos", "position"))
class Composite2():
def __init__(self, x, y):
self._delegatee = Delegatee(x, y)
Now that's a lot of code and indirection levels for something that is basically very simple, so it only makes sense if you really have a lot of delegations to set up - from experience, the simplest the code, the easier it is to read, understand, test and debug (python zen: "simple is better than complex"), and "smart" solutions often become a PITA in the long term (been here, done that, now I know better).