[Python-ideas] Composition over Inheritance
Wes Turner
wes.turner at gmail.com
Mon Oct 30 03:12:58 EDT 2017
On Monday, October 30, 2017, Wes Turner <wes.turner at gmail.com> wrote:
> ... 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
> <javascript:_e(%7B%7D,'cvml','wes.turner at gmail.com');>> wrote:
>
>>
>>
>> On Sunday, October 29, 2017, Nick Coghlan <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/objec
>> ts-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/objec
>> ts-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
>>>
>>
https://docs.python.org/3/howto/descriptor.html ...
http://python-reference.readthedocs.io/en/latest/docs/dunderdsc/#descriptor-protocol
""'
Instance BindingIf binding to a new-style object instance, a.x is
transformed into the call: type(a).__dict__[‘x’].__get__(a, type(a)).
"""
>
>>
>>>
>>> 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/3c08c1d2/attachment-0001.html>
More information about the Python-ideas
mailing list