
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.