top | item 37283594

(no title)

yayachiken | 2 years ago

For everybody reading this and scratching their head why this is relevant: Python subclassing is strange.

Essentially super().__init__() will resolve to a statically unknowable class at run-time because super() refers to the next class in the MRO. Knowing what class you will call is essentially unknowable as soon as you accept that either your provider class hierarchy may change or you have consumers you do not control. And probably even worse, you aren't even guaranteed that the class calling your constructor will be one of your subclasses.

Which is why for example super().__init__() is pretty much mandatory to have as soon as you expect that your class will be inherited from. That applies even if your class inherits only from object, which has an __init__() that is guaranteed to be a nop. Because you may not even be calling object.__init__() but rather some sibling.

So the easiest way to solve this is: Declare everything you need as keyword argument, but then only give **kwargs in your function signature to allow your __init__() to handle any set of arguments your children or siblings may throw at you. Then remove all of "your" arguments via kwargs.pop('argname') before calling super().__init__() in case your parent or uncle does not use this kwargs trick and would complain about unknown arguments. Only then pass on the cleaned kwargs to your MRO foster parent.

So while using **kwargs seems kind of lazy, there is good arguments, why you cannot completely avoid it in all codebases without major rework to pre-existing class hierarchies.

For the obvious question "Why on earth?" These semantics allow us to resolve diamond dependencies without forcing the user to use interfaces or traits or throwing runtime errors as soon as something does not resolve cleanly (which would all not fit well into the Python typing philosophy.)

discuss

order

patrickkidger|2 years ago

FWIW, I've come to regard this (cooperative multiple inheritance) as a failed experiment. It's just been too confusing, and hasn't seen adoption.

Instead, I've come to prefer a style I took from Julia: every class is either (a) abstract, or (b) concrete and final.

Abstract classes exist to declare interfaces.

__init__ methods only exist on concrete classes. After that it should be thought of as unsubclassable, and concerns about inheritance and diamond dependencies etc just don't exist.

(If you do need to extend some functionality: prefer composition over inheritance.)

bowsamic|2 years ago

This is why I hate Python, absolutely none of this is obvious from the design of the language

LBTables|2 years ago

At an even more basic level, the lack of static typing seems like such a tradeoff getting an incredibly huge nuisance in readability and stupid runtime bugs that shouldn't be a thing in exchange for a feature that's rarely useful.

Granted, I'm primarily an embedded developer. Can any Python experts explain to me a highly impactful benefit of dynamic typing?

throwaway2037|2 years ago

To add to your list: During string concatenation, there is no automatic conversion to string. It results in an exception. It is infuriating.

This code:

    "abc" + 123
... will raise this exception:

    TypeError: can only concatenate str (not "int") to str
I have wasted so many hours fixing this same bug, over and over again.

Izkata|2 years ago

> So the easiest way to solve this is: Declare everything you need as keyword argument, but then only give *kwargs in your function signature to allow your __init__() to handle any set of arguments your children or siblings may throw at you. Then remove all of "your" arguments via kwargs.pop('argname') before calling super().__init__() in case your parent or uncle does not use this kwargs trick and would complain about unknown arguments. Only then pass on the cleaned kwargs to your MRO foster parent.

The easiest way is to not put your arguments into kwargs in the first place. If you put them as regular function arguments (probably give them a default value so they look like they're related to kwargs), then the python runtime separates them from the rest when it generates kwargs and you don't have to do the ".pop()" part at all.

sbrother|2 years ago

Thank you for explaining this; there are a lot of comments here suggesting trivial code style improvements for use cases where *kwargs wasn’t actually needed. The more interesting question is how to improve the use case you describe — which is how I’ve usually seen *kwargs used.

dontlaugh|2 years ago

Having used Python a lot, I was never glad for multiple inheritance. I’d prefer traits.

throwaway2037|2 years ago

Are traits and mixins the same? If not, can you please provide a trivial example. It would be useful to better understand what you mean. When I was very young, learning C++, I thought multiple inheritance was so cool. Now, I know it is like sleeping in the open jaws of a saltwater croc.