[Python-ideas] Composition over Inheritance

Nick Coghlan ncoghlan at gmail.com
Sun Oct 29 00:28:01 EDT 2017


On 29 October 2017 at 12:25, Brendan Barnwell <brenbarn at brenbarn.net> wrote:

> On 2017-10-28 19:13, Soni L. wrote:
>
>> And to have all cars have engines, you'd do:
>>
>> class Car:
>>     def __init__(self, ???):
>>       self[Engine] = GasEngine()
>>
>> car = Car()
>> car[Engine].kickstart() # kickstart gets the car as second argument.
>>
>> And if you can't do that, then you can't yet do what I'm proposing, and
>> thus the proposal makes sense, even if it still needs some refining...
>>
>
>         As near as I can tell you can indeed do that, although it's still
> not clear to me why you'd want to.  You can give Car a __getitem__ that
> on-the-fly generates an Engine object that knows which Car it is attached
> to, and then you can make Engine.kickstart a descriptor that knows which
> Engine it is attached to, and from that can figure out which Car it is
> attached to.
>

Right, I think a few different things are getting confused here related to
how different folks use composition.

For most data modeling use cases, the composition model you want is either
a tree or an acyclic graph, where the subcomponents don't know anything
about the whole that they're a part of. This gives you good component
isolation, and avoids circular dependencies.

However, for other cases, you *do* want the child object to be aware of the
parent - XML etrees are a classic example of this, where we want to allow
navigation back up the tree, so each node gains a reference to its parent
node. This often takes the form of a combination of delegation
(parent->child references) and dependency inversion (child->parent
reference).

For the car/engine example, this relates to explicitly modeling the
relationship whereby a car can have one or more engines (but the engine may
not currently be installed), while an engine can be installed in at most
one car at any given point in time.

You don't even need the descriptor protocol for that though, you just need
the subcomponent to accept the parent reference as a constructor parameter:

    class Car:
      def __init__(self, engine_type):
        self.engine = engine_type(self)

However, this form of explicit dependency inversion wouldn't work as well
if you want to be able to explicitly create an "uninstalled engine"
instance, and then pass the engine in as a parameter to the class
constructor:

    class Car:
      def __init__(self, engine):
        self.engine = engine # How would we ensure the engine is marked as
installed here?

As it turns out, Python doesn't need new syntax for this either, as it's
all already baked into the regular attribute access syntax, whereby
descriptor methods get passed a reference not only to the descriptor, but
*also* to the object being accessed:
https://docs.python.org/3/howto/descriptor.html#descriptor-protocol

And then the property builtin lets you ignore the existence of the
descriptor object entirely, and only care about the original object,
allowing the above example to be written as:

    class Car:
      def __init__(self, engine):
        self.engine = engine # This implicitly marks the engine as installed

      @property
      def engine(self):
          return self._engine

      @engine.setter
      def engine(self, engine):
         if engine is not None:
             if self._engine is not None:
                 raise RuntimeError("Car already has an engine installed")
             if engine._car is not None:
                 raise RuntimeError("Engine is already installed in another
car")
             engine._car = self
         self._engine = engine

     car = Car(GasEngine())

ORMs use this kind of descriptor based composition management extensively
in order to reliably model database foreign key relationships in a way
that's mostly transparent to users of the ORM classes.

Cheers,
Nick.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20171029/6fcf6f02/attachment-0001.html>


More information about the Python-ideas mailing list