[Python-ideas] Composition over Inheritance
Wes Turner
wes.turner at gmail.com
Mon Oct 30 02:40:31 EDT 2017
... But interfaces are clunky and traits are lightweight, and this isn't
Go, so we can't just create a class as a namespace full of @staticmethods
which accept the relevant object references.
* __setattribute__ -> __getitem__, __setitem__
On Monday, October 30, 2017, Wes Turner <wes.turner at gmail.com> wrote:
>
>
> On Sunday, October 29, 2017, Nick Coghlan <ncoghlan at gmail.com
> <javascript:_e(%7B%7D,'cvml','ncoghlan at gmail.com');>> wrote:
>
>> 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).
>>
>
> This is Java-y and maybe not opcode optimizable, but maybe there's a case
> for defining __setattribute__ so that square brackets denote Rust-like
> traits:
>
> https://docs.spring.io/spring-python/1.2.x/sphinx/html/
> objects-pythonconfig.html#object-definition-inheritance
>
> @Object(parent="request")
> def request_dev(self, req=None):
>
> > Observe that in the following example the child definitions must
> define an optional ‘req’ argument; in runtime they will be passed its value
> basing on what their parent object will return.
>
> It's testable, but confusing to Java programmers who aren't familiar with
> why Guice forces the patterns that it does:
>
> https://docs.spring.io/spring-python/1.2.x/sphinx/html/
> objects-more.html#testable-code
>
> https://github.com/google/guice/wiki/Motivation#dependency-injection
>
> > Like the factory, dependency injection is just a design pattern. The
> core principle is to separate behaviour from dependency resolution. In our
> example, the RealBillingService is not responsible for looking up the
> TransactionLog and CreditCardProcessor. Instead, they're passed in as
> constructor parameters:
>
> When these are constructor parameters, we don't need to monkeypatch attrs
> in order to write tests; which, IIUC, is also partly why you'd want
> traits/mixins with the proposed special Rust-like syntax:
>
> https://docs.pytest.org/en/latest/monkeypatch.html
>
> https://docs.pytest.org/en/latest/fixture.html#modularity-using-fixtures-
> from-a-fixture-function (this is too magic(), too)
>
> But you want dynamic mixins that have an upward reference and Rust-like
> syntax (and no factories).
>
>
>> For the car/engine example, this relates to explicitly modeling the
>> relationship whereby a car can have one or more engines
>>
>
> class MultiEngine():
> zope.interface.implements(IEngine)
>
> https://zopeinterface.readthedocs.io/en/latest/README.html#declaring-
> implemented-interfaces
>
> But interfaces aren't yet justified because it's only a few lines and
> those are just documentation or a too-complex adapter registry dict, anyway.
>
>
>> (but the engine may not currently be installed),
>>
>
> So it should default to a MockEngine which also implements(IEngine) and
> raises NotImplementedError
>
>
>>
>> while an engine can be installed in at most one car at any given point
>> in time.
>>
>
> But the refcounts would be too difficult
>
> This:
>
>
>>
>> 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/howt
>> o/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())
>>
>
> This could be less verbose. And less likely to raise a RuntimeError.
>
>
>>
>> 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.
>>
>
> So there's a 'factory' which passes the ref as a constructor parameter for
> such ORM instances; but they generally don't need to be dynamically
> modified at runtime because traits.
>
>
>>
>> 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/20171030/e2f58344/attachment.html>
More information about the Python-ideas
mailing list