On Tue, Jun 22, 2021 at 10:40 AM Steven D'Aprano email@example.com wrote:
True, all true, but considering that this is *not* actually part of the class, some of that doesn't really apply. For instance, is it really encapsulation? What does that word even mean when you're injecting methods in from the outside?
Sure it's encapsulation. We can already do this with non-builtin classes:
class SpammySpam: def spam(self, arg): ... from another_module import eggy_method def aardvarks(self, foo, bar): ... SpammySpam.aardvarks = aardvarks
The fact that two of those methods have source code that wasn't indented under the class statement is neither here nor there. Even the fact that eggy_method was defined in another module is irrelevant. What matters is that once I've put the class together, all three methods are fully encapsulated into the SpammySpam class, and other classes can define different methods with the same name.
Encapsulation is less about where you write the source code, and more about the fact that I can have
without the two spam methods stomping on each other.
Hmm, that's not what I'd usually understand "encapsulation" to mean. That's what would normally be called "namespacing".
"... encapsulation refers to the bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object's components." https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
Extension methods let us extend classes without the downsides of monkey-patching. Extension methods are completely opt-in while monkey-patching is mandatory for everyone. If we could only have one, extension methods would clearly be the safer choice.
I don't think it's safer necessarily. With this proposal, we have the notion that obj.method() can mean two completely different things *at the same time* and *on the same object* depending on how you refactor the code.
# file1.py from file2 import func # and apply some extension methods def spamify(obj): print(obj.method()) print(func(obj))
# file2.py def func(obj): return obj.method()
Is that really beneficial? All I'm seeing is myriad ways for things to get confusing - just like in the very worst examples of monkey-patching.
And yes, I have some experience of monkey-patching in Python, including a situation where I couldn't just "import A; import B", I had to first import a helper for A, then import B, and finally import A, because there were conflicting monkey-patches. But here's the thing: extension methods (by this pattern) would not have solved it, because the entire *point* of the monkey-patch was to fix an incompatibility. So it HAD to apply to a completely different module.
That's why, despite its problems, I still think that monkey-patching is the cleaner option. It prevents objects from becoming context-dependent.
We don't make heavy use of monkey-patching, not because it isn't a useful technique, but because:
unlike Ruby, we can't extend builtins without subclassing;
we're very aware that monkey-patching is a massively powerful technique with huge foot-gun potential;
and most of all, the Python community is a hell of a lot more conservative than Ruby.
And the Ruby community is starting to see the risks of monkey-patching. (There's a quiz floating around the internet - "Ruby or Rails?" - that brings into sharp relief the incredibly far-reaching effects of using Rails. It includes quite a few methods on builtin objects.) So I am absolutely fine with being conservative.
We have import hooks and MacroPy. Does anyone use them in production? I certainly don't - not because I can't, but because I won't without a VERY good reason.
Even basic techniques intentionally added to the language (like being able to attach attributes onto function objects) are often looked at as if they were the worst kind of obfuscated self-modifying code. Even when those same techniques are used in the stdlib people are still reluctant to use it. As a community, we're like cats: anything new and different scares us, even if its actually been used for 30 years.
We're a risk-adverse community.
I'm not sure why attaching attributes to functions is frowned upon; I'd personally make very good use of this for static variables, if only I could dependably refer to "this_function". But risk-averse is definitely preferable to the alternative. It means that Python is a language that can be learned as a whole, rather than being fragmented into "the NumPy flavour of Python" and "the Flask flavour of Python" and so on, with their own changes to the fabric of the language.
So far, I'm not seeing anything in extension methods to make me want to change that stance.