malmiteria  writes:

 > to dive into conceptual ideas a bit more:

I'm not sure why you assume that nobody knows this stuff, at least at
the extremely high and fuzzy level of your discussion.

Executive summary: I see a lot of theory in your posts, but it's very
difficult to tie it to *my* practice, and you don't go into any detail
about *your* practice that requires the features you propose.

 > inheritance is usually what we do when we mean *is a*

This really isn't very useful.  Much more useful would be

    Class Cat should inherit from class Animal if all concrete
    attributes of Animal are appropriate for Cat, and all abstract
    attributes of Animal can be instantiated appropriately for Cat.
    *** This is what we mean when we say a cat *is an* animal. ***
    The more you reduce "all" to "most", the less appropriate
    inheritance is.

 > In such a case with multiple *is a* there's multiple strategies to
 > blend in the multiple *is a*.

This is true.  Given the rarity of multiple inheritance, and
especially the rarity of cases where the C3 MRO "gets it wrong", you
need to do a lot more than just point out the multiplicity.  You have
to demonstrate a need to support other cases.

IMO, it's *not* important to support the cases where all of the parent
methods of the same name should be called by automatically calling all
of them.  In Python, it's very easy to implement this directly if
necessary.  On the other hand, if we default to calling all parent
methods of the same name, it's very difficult to work around that,
even impossible if you can't modify the parent classes.  And this is
subject to one the same complaint that you make about the MRO, as for
some methods you might want all parent methods called, and others you
want only one (or a larger proper subset).

 > Today's MRO implicitely overrides all method from the second parent
 > with method from the first parent.
 > Essentially, the first parent (in declaration order) is dominant on
 > all attributes.
 > This isn't very subtle, as we could want some attribute of each
 > parent to be dominant, while others to be recessive.

It may not be flexible, but it is quite useful.  In most cases it's
quite trivial to pick up the version from the second parent:

    class C(A, B):
        def foo(self, *args):
            return B.foo(self, *args)
        # we don't define bar() because we want C to inherit A's
        # version

But in my experience it's rarely needed.  Mostly, there's a primary
parent class that provides most of the functionality, and there are
skeletal mixins which have one job each.  YMMV.

 > My "can't assume one parent is more specialised"

As I've pointed out before, Python makes no such assumption.  Python
*forces* you to specify the order of parent classes, and defines the
semantics so that each parent takes precedence over any that follow it
in the class declaration.  Python assumes that will Just Work for you.
If it doesn't, you have to curse Python and work around it.  Python
shrugs your curse right off, though.

The questions are

1.  Are there any "!#$%ing Python" cases that cannot be worked around?
2.  How frequent are the "!#$%ing Python" cases compared to the Just
    Works cases?
3.  How painful are the workarounds?

AFAICS, you have consistently doubled down on the nonsense quoted
above, and have not even tried to answer any of questions 1-3 with
concrete examples of code somebody actually uses.  The exception is
your story about your colleague's issue with order of parents in one
class in one Django application.  The answers in that story as you
told it are

1.  Not here.
2.  This was not a "!#$%ing Python" case.
3.  No workarounds needed, just don't fix what ain't broke.

 > That's why my proposal for those cases is to allow inheritance to
 > be postponed after definition time, and set by the final child
 > class that inherits from those.
 > 
 > ```
 > class Sphynx(Hairless(Cat)): pass
 > ```

That doesn't "look like" a mixin, though.  That looks like a simple
inheritance chain, and if so the "(Cat)" is redundant and Hairless is
very badly named because it doesn't tell you it's a cat.  Sure, you
can tell me Hairless is a mixin applied to Cat, and Hairless does not
inherit from Cat, but I'm still going to WTF every time I see it.  Not
sure what this "postponed inheritance" is supposed to mean, but that's
not a good way to write it, I'm pretty sure.

Also, Sphynx is a pretty horrible example.  Given the diversity of the
animal kingdom, Animal is almost certainly a (very) abstract base
class, with almost all characteristics added by composition.  Even
within the Felidae, there is an awful lot of variation.  So I would
expect Hairless to not even exist, and for Sphynx to look like:

    class Sphynx(Cat):
        def __init__(self, *args):
            super().__init__(*args)
            self.hair = None

You could make Hairless a mixin class:

    class Hairless:
        def __init__(self, hair=None):
            self.hair = None

    class Sphynx(Hairless, Cat):     # why this order?
        def __init__(self, *args):
            super(Hairless, self).__init__(*args)
            super().__init__(*args)

but I suspect that would make you extremely unpopular in code review.

 > [Decorators are a thing,]
 > So i guess using the @ syntax could be meaningful too:
 > ```
 > class Sphynx(Cat @ Hairless): pass
 > ```

except that decorator syntax doesn't look anything like that, so it's
not suggestive.

 > This makes it easy to chain lots of mixins without having to add
 > tons of parenthesis, so it might be a better syntax that the one i
 > originally proposed.

But conceptually mixins aren't chained, they're composed.  The
primitive idea of mixins isn't "but with" (in the sense of an existing
attribute being different), it's "but also with" (in the sense of
adding an attribute).  So in this sense, using mixins with inheritance
is inconsistent with their compositional nature.  Python being what it
is, we can't stop programmers from using mixins to change existing
behavior, but it's kinda incoherent IMO.

In practice in the projects I've worked in, inheriting mixins is often
simply a matter of notational convenience.  The attribute and ancestor
sets of A and B don't intersect, so

    class C:
        def __init__(self):
            self.a = A()
            self.b = B()
        def __getattribute__(self, attrname):
            try:
                return getattr(self.a, attrname)
            except AttributeError:
                return getattr(self.b, attrname)

actually makes complete sense, but

    class D(A, B):
        pass

has equivalent semantics, instances of D and C are used exactly the
same way, but D expresses it more clearly.  It is true that it's often
possible to design such mixins cooperatively.  That is, by duck-typing
self, they can share a resource provided by a child class.  Typically
this resource is inherited by the child from a "primary" parent class,
but the mixin class does *not* need to be a descendant of the primary
parent class (or any of its ancestors, for that matter), it only needs
to know the name of the resource it's supposed to use or manipulate.[1]
To me, this still feels more like composition than inheritance.

In fact, I suspect multiple true parents is relatively rare.  For
example,

class FlyingAnimal(Animal): pass
class Mammal(Animal): pass
class Bat(Mammal, FlyingAnimal): pass

looks reasonable.  But I find it hard to treat hairless cats the same
way.  I don't think "sphynx is-a hairless" independently of "sphynx
is-a cat", or at least "sphynx is-a mammal".  It's not useful, in the
sense that animals where "hairless" doesn't make sense abound.  We
don't talk of hairless reptiles or fish. On the other hand, while
paramecia have "hair" and other protozoa don't, I'm not sure I want to
think of hair (or not) on a protozoan and hair (or not) on a cat as
"the same difference."

 > And overall, having to switch from a *is a* relationship to a *has
 > a* relationship, simply because the langage doesn't allow *is a* in
 > some cases hints at excessive limitations of the langage.

You would have to be specific about the examples where is-a is
disallowed.  I'm sure that in some of the cases, composition was
recommended because it's just a better way to think about the example
than inheritance is.


Footnotes: 
[1]  Of course it may be safer to have a common ancestor whose only
role is to provide the resource in such a way as to coordinate the
child and the mixins.  "Python's super() considered super!" provides
an example of how to do this.

_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/QX5BRATKMYGNU67NHOK4IVZAAPA3YXEL/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to