[Python-ideas] Composition over Inheritance

Soni L. fakedme+py at gmail.com
Sat Oct 28 16:24:43 EDT 2017



On 2017-10-28 02:51 PM, Steven D'Aprano wrote:
> On Sat, Oct 28, 2017 at 10:19:09AM -0200, Soni L. wrote:
>
>>> class Car:
>>>      def __init__(self):
>>>          self.engine = Engine()
>>>          self.accelerator = AcceleratorPedal()
>>>          ...
>>>
>>>      def start(self):
>>>          # Delegate to the ignition component.
>>>          self.ignition.start()
>>>
>>>
>>> etc. Obviously this is just a very loose sketch, don't take it too
>>> literally. Is this the sort of thing you are talking about?
>> So how do you call car.ignition.start() from car.key.turn()?
> You don't -- the key is not a component of the car, its an argument of
> ignition.start. If the key doesn't fit, ignition.start() raises an
> exception and the car doesn't start.
>
> I'm not really interested in getting into a tedious debate over the best
> way to design a Car object. As I said, the above is just a loose sketch
> illustrating composition, not a carefully planned and debugged object.
> The aim here is to understand your proposal: what exactly do you mean
> for Python to support composition over inheritence, and how does it
> differ from Python's existing support for composition?
>
> You ignored my question: Is that the sort of thing you mean by
> composition? If not, then what do you mean by it? This is not a
> rhetorical question: I'm having difficulty understanding your proposal.
> It is too vague, and you are using terminology in ways I don't
> understand.
>
> Maybe that's my ignorance, or maybe you're using non-standard
> terminology. Either way, if I'm having trouble, probably others are too.
> Help us understand your proposal.

With composition, you can have car.key.turn() call car.ignition.start(), 
without having to add car to key or ignition to key. You just have to 
put both in a car and they can then see eachother!

>
>
>>>> I am not sure how you'd set components, or test for components,
>>> If you don't know how to set components, or test for them, what do you
>>> know how to do with components?
>>>
>>> And how are components different from attributes?
>> They're more like conflict-free interfaces, and in this specific case
>> they're designed with duck typing in mind. (You can dynamically add and
>> remove components, and use whatever you want as the component. You
>> cannot do that with inheritance.)
> What do you mean by "conflict-free interfaces"?
>
> I can only repeat my earlier request:
>
>>> If might help if you give a concrete example, with meaningful names. It
>>> would help even better if you can contrast the way we do composition now
>>> with the way you think we should do it.

In Rust, you can put as many "conflicting" traits as you want on the 
same object, and it'll still work! It'll compile, it'll run, you'll be 
able to use the object in existing code that expects a specific trait, 
and code that operates on the object itself is fairly easy to write.

>
>>> I'm afraid that at the moment I'm parsing your post as:
>>>
>>> "composition is cool, we should use it; and o.[c].m() is cool syntax, we
>>> should use it for composition; I'll leave the details to others".
>> Again, how do you call car.ignition.start() from car.key.turn()?
> Maybe you can't. Maybe this is a crippling example of why composition
> isn't as good as inheritence and the OOP community is right that
> inheritence is the best thing since sliced bread. Maybe my design of the
> Car object sucks.
>
> But who cares? None of this comes any closer to explaining your
> proposal.

Composition works fine, if you do it like Rust.

>
>
>>>> Thus I think o.[c].m() should be syntax sugar for o[c].m(o), with o
>>>> being evaluated only once,
>>> I don't see why you're using __getitem__ instead of attribute access;
>>> nor do I understand why m gets o as argument instead of c.
>>>
>>> Wait... is this something to do with Lieberman-style delegation?
>>>
>>> http://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html
>>>
>>> http://code.activestate.com/recipes/519639-true-lieberman-style-delegation-in-python/
>>>
>> TL;DR. But no, it's not some form of delegation.
> One of us is using non-standard terminology, and I don't think it is me.
> (Happy to be corrected if I'm wrong.)
>
> I understand that composition and delegation go hand in hand: you can't
> have one without the other. Composition refers to the arrangement of an
> object that is composed of other objects. Delegation refers to the way
> that the compound object calls methods on the component objects.
>
> The point (as I understand it) of composition is that a Car doesn't just
> have an Engine, it delegates functionality to the Engine: the Car object
> derives functionality by calling Engine methods directly, rather than
> inheriting them. Car.forward() delegates to Engine.forward().
>
> The point is that the implementation of Car.forward is found in the
> self.engine object, rather than being inherited from an Engine class.
>
> Without delegation, the components aren't components at all, merely data
> attributes: Car.colour = 'red'.
>
> Does this match your proposal? If not, how is your proposal different?

Meet ECS:

https://en.wikipedia.org/wiki/Entity_component_system

Now, keep in mind, the usual ECS is crap. Why's it crap? Because there's 
no reasonable call convention that's also performant!

System.action(entity) # crap. can't override.
entity.action() # crap. conflicts easily. either not very dynamic or 
awful semantics.
entity.get(System).action(entity) # crap. while you can override this 
one, you get to evaluate entity twice!
entity.get(System).action() # crap. creates an object every time it's 
used (or hogs some RAM to cache the object).

I could keep going but the point is that Rust traits solve all these 
problems if you let them.

>
>
>> It still gets `self` (which is whatever is in o[c] - which may be c
>> itself, or an arbitrary object that fulfills the contract defined by c),
>> but also gets `o` in addition to `self`. (Unless it's a plain function,
>> in which case it gets no `self`.)
> That sounds like a hybrid of Lieberman-style delegation and the more
> common form. At first glance, that seems to add complexity without
> giving the advantages of either form of delegation.

It's not delegation.

>
>
>>>> as that solves a lot of current issues
>>>> relating to inheritance while introducing very few issues relating to
>>>> python's "everything is separate" (e.g. __get__ vs __getattr__)
>>>> policy.This also makes setting components and testing for components
>>>> fairly trivial, and completely avoids the issues mentioned above by
>>>> making their syntax illegal.
>>> Above you said that you don't know how to set and test for components,
>>> now you say that doing so is trivial. Which is it?
>> If you pay closer attention, you'll notice the two different paragraphs
>> talk about two different syntaxes.
> I don't care about syntax yet. I'm still trying to understand the
> semantics of your proposal. Whether you spell this thing
>
>      instance.[component]
>
>      get_component(instance, 'component')
>
>      instance!component
>
> is less important than understand what it *does*.

Semantics is Rust traits at runtime, without the delegation (this is by 
design - with delegation, libraries could make assumptions, and adding 
new traits to an object could break them).

>
>
>> - o.[c] as a standalone syntax element, allowing things like
>> x=o.[c1].[c2]; and x=o.[c1][c2];.
>> - o.[c].m() as a standalone syntax element, *disallowing* the above.
> That makes no sense to me. I cannot make head or tail of what that is
> supposed to mean.

It means whether your parser looks for "o.[c]" and emits an opcode for 
it, or it looks for "o.[c].m()" and emits an opcode for that instead.

>
>
>>>> (Disclaimer: This was inspired by my own programming language,
>>>> Cratera[1], so I'm a bit biased here. Cratera was, in turn, inspired by
>>>> Rust[2] traits.
>>> Traits are normally considered to be a more restricted, safer form of
>>> multiple inheritence, similar to mixins but even more restrictive.
>> What do you mean more restricted?
> I mean that if you have two traits with the same method:
>
> class SpamTrait:
>      def foo(self): ...
>
> class EggTrait:
>      def foo(self): ...
>
>
> then you cannot use them both in a single class:
>
> class MyClass(SpamTrait, EggTrait):
>      ...
>
> since the foo method clashes, unless MyClass explicitly specifies which
> foo method to use. Mixins and regular multiple inheritence do not have
> that restriction.
>
> If you expect both foo methods to be called, that's just regular
> multiple inheritence, with all its complexity and disadvantages.
> (See Michele Simionato numerous posts on Artima about super, multiple
> inheritence, mixins and his own traits implementation.)
>
> The point of traits is to prevent the existence of such conflicts: either by
> prohibiting the use of both SpamTrait and EggTrait at the same time, or
> by forcing MyClass to explicitly choose which foo method gets used.
> That's safer than unrestricted mixins and multiple inheritence, since it
> reduces the complexity of the inheritence heirarchy.
>
>
>> They let you have the same method in multiple components/traits and
>> not have them conflict, among other things.
> I think we are in agreement here.
>
> But in any case... traits are a form of inheritence, not composition.
> You said this proposal is inspired by Rust traits. Can you explain the
> connection between inheritence of traits and composition?
>

Rust traits are the best traits.

You can have a Rust struct implement 2 traits with the same method. And 
it just works. You can call each trait method separately - 
disambiguation is done at the call site. This doesn't translate well to 
a dynamic language, so, for python, it's best to always specify the 
trait (no delegation - o.m() is always o.m() and if you want o.[c].m() 
you need to be explicit about it).

Otherwise, it's the same as always: called method gets main object, etc.

(Disclaimer: I wrote this message starting with the last question. It 
should still make sense tho. PS: You should go play with some Rust. As 
far as compiled languages go, Rust is pretty much the best.)

>



More information about the Python-ideas mailing list