Pythonicity: Composition vs. Inheritance
coady.github.io
coady.github.io
You often get a ton of methods / attributes on the child class, which gets hard to keep tabs on. You have to be careful not to use the same names for things. (The worst is that someone uses this broken encapsulation and relies on things across parent classes via the child).
I get that inheritance can go okay for simple cases, but have seen enough chaotic uses of it that I'd just encourage people to write the boilerplate for forwarding methods.
Inheritance turns the functional structure of your code into mud. Unless inheritance is absolutely the best design, better to drop it.
What I also don't understand is how you're supposed to instantiate the new NetworkElement from an existing jnettool.tools.elements.NetworkElement. With delegation it's easy (just construct a wrapper with the object to be wrapped), but if the library is giving you instances of jnettool.tools.elements.NetworkElement, how do you get instances of your own subclass instead?
https://www.nongnu.org/txr/txr-manpage.html#N-92252A14
https://www.nongnu.org/txr/txr-manpage.html#N-5F56EB59
These are themselves written via define-struct-clause, which provides a macro mechanism for writing new kinds of clauses that can appear in a defstruct:
https://www.nongnu.org/txr/txr-manpage.html#N-91BFF63B
define-struct-clause itself provides a kind of alternative to manually doing composition and inheritance: you just define some new clauses to do whatever is necessary and use them in the target classes.
A custom clause can cause the target class to inherit bases. What makes that possible is that inheritance is not only expressed using (defstruct duck (animal ...)) but alternatively via a an (:inherit ...) clause. Thus clause macros can generate (:inherit ...), thereby injecting bases.
In fact, inheritance has nothing to do with reusability (or flexibility): it's point is to provide typing hierarchy, so the code can follow the Liskov substitution principle.
In "traditional" Python, which is dynamically typed and never statically analysed, there were no built-in controls over what is being passed at runtime, so the only real purpose for inheritance was to reuse code. This changeds with the introduction of typing annotations -- while the runtime is still fully dynamic, you can use mypy and other static analysis tools to minimise type-related errors before deploying.
In fact, Python has a great way to both support composition and support interfaces without requiring inheritance: a combination of typing.Protocol and mixins.
Scala 3 has export statements (https://docs.scala-lang.org/scala3/reference/other-new-featu...). I wish more OOP languages had something similar, so that there was less boilerplate when composing other objects.
And it is possible to implement something like that in python as a library, although a with a quick search I only found a couple of blog posts on how to implement something yourself.