Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way of doing composition aliases

Tags:

python

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
like image 999
Granitosaurus Avatar asked Mar 08 '20 06:03

Granitosaurus


2 Answers

There are a number of problems with the first method:

  1. The alias won't update when the attribute it refers to changes unless you jump through extra hoops. You could, for example, make 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.
  2. 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.
  3. 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
like image 136
Mad Physicist Avatar answered Nov 15 '22 17:11

Mad Physicist


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.

like image 40
juanpa.arrivillaga Avatar answered Nov 15 '22 18:11

juanpa.arrivillaga