What is the most pythonic and correct way of doing composition aliases?
Here's a hypothetical scenario:
class House:
def cleanup(self, arg1, arg2, kwarg1=False):
# do something
class Person:
def __init__(self, house):
self.house = house
# aliases house.cleanup
# 1.
self.cleanup_house = self.house.cleanup
# 2.
def cleanup_house(self, arg1, arg2, kwarg1=False):
return self.house.cleanup(arg1=arg1, arg2=arg2, kwarg1=kwarg1)
AFAIK with #1 my tested editors understand these just as fine as #2 - auto completion, doc strings etc.
Are there any down-sides to #1 approach? Which way is more correct from python's point of view?
To expand on method #1 unsettable and type hinted variant would be immune to all of the issues pointed out in the comments:
class House:
def cleanup(self, arg1, arg2, kwarg1=False):
"""clean house is nice to live in!"""
pass
class Person:
def __init__(self, house: House):
self._house = house
# aliases
self.cleanup_house = self.house.cleanup
@property
def house(self):
return self._house
There are a number of problems with the first method:
house
a property
with a setter, but that is non-trivial work for something that shouldn't require it. See the end of this answer for a sample implementation.cleanup_house
will not be inheritable. A function object defined in a class is a non-data descriptor that can be inherited and overridden, as well as be bound to an instance. An instance attribute like in the first approach is not present in the class at all. The fact that it is a bound method is incidental. A child class will not be able to access super().cleanup_house
, for a concrete example.person.cleanup_house.__name__ != 'cleanup_house'
. This is not something you check often, but when you do, you would expect the function name to be cleanup
.The good news is that you don't have to repeat signatures multiple times to use approach #2. Python offers the very convenient splat (*
)/splatty-splat (**
) notation for delegating all argument checking to the method being wrapped:
def cleanup_house(self, *args, **kwargs):
return self.house.cleanup(*args, **kwargs)
And that's it. All regular and default arguments are passed through as-is.
This is the reason that #2 is by far the more pythonic approach. I have no idea how it will interact with editors that support type hints unless you copy the method signature.
One thing that may be a problem is that cleanup_house.__doc__
is not the same as house.cleanup.__doc__
. This could potentially merit a conversion of house
to a property
, whose setter assigns cleanup_house.__doc__
.
To address issue 1. (but not 2. or 3.), you can implement house
as a property with a setter. The idea is to update the aliases whenever the house
attribute changes. This is not a good idea in general, but here is an alternative implementation to what you have in the question that will likely work a bit better:
class House:
def cleanup(self, arg1, arg2, kwarg1=False):
"""clean house is nice to live in!"""
pass
class Person:
def __init__(self, house: House):
self.house = house # use the property here
@property
def house(self):
return self._house
@house.setter
def house(self, value):
self._house = house
self.cleanup_house = self._house.cleanup
I just wanted to add one more approach here, which if you want house
to be public and settable (I would generally treat such a thing as immutable), you can make cleanup_house
the property, like so:
class House:
def cleanup(self, arg1, arg2, kwarg1=False):
"""clean house is nice to live in!"""
print('cleaning house')
class Person:
def __init__(self, house: House):
self.house = house
@property
def cleanup_house(self):
return self.house.cleanup
At least in an Ipython REPL, the code-completion and docstring seems to be working as you'd hope. Note sure how it would interact with type annotations...
EDIT: so, mypy 0.740 at least cannot infer the type signature of person.cleanup_house
, so that's not great, although, it's not surprising:
(py38) Juans-MBP:workspace juan$ cat test_mypy.py
class House:
def cleanup(self, arg1:int, arg2:bool):
"""clean house is nice to live in!"""
print('cleaning house')
class Person:
house: House
def __init__(self, house: House):
self.house = house # use the property here
@property
def cleanup_house(self):
return self.house.cleanup
person = Person(House())
person.cleanup_house(1, True)
person.cleanup_house('Foo', 'Bar')
reveal_type(person.cleanup_house)
reveal_type(person.house.cleanup)
(py38) Juans-MBP:workspace juan$ mypy test_mypy.py
test_mypy.py:19: note: Revealed type is 'Any'
test_mypy.py:20: note: Revealed type is 'def (arg1: builtins.int, arg2: builtins.bool) -> Any'
I would still just go with #2.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With