On Tue, Jun 22, 2021 at 05:50:48PM +1000, Chris Angelico wrote:
Hmm, that's not what I'd usually understand "encapsulation" to mean. That's what would normally be called "namespacing".
Pfft, who you going to believe, me or some random folx on the internet editing Wikipedia? *wink*
Okay, using the Wikipedia/OOP definition still applies. Presumably most extension methods are going to be regular instance methods or class methods, rather than staticmethod. So they will take a `self` (or `cls`) parameter, and presumably most such extension methods will actually act on that self parameter in some way.
There is your "bundling of data with the methods that operate on that data", as required :-)
The fact that the methods happen to be written in an separate file, and (in some sense) added to the class as extension methods, is neither here nor there. While we *could* write an extension method that totally ignored `self` and instead operated entirely on global variables, most people won't -- and besides, we can already do that with regular methods.
class Weird: def method(self, arg): global data, more_data, unbundled_data, extra_data del self # don't need it, don't want it do_stuff_with(data, more_data, unbundled_data, extra_data)
So the point is that extension methods are no less object-orientey than regular methods. They ought to behave just like regular methods with respect to encapsulation, namespacing, inheritance etc, modulo any minor and necessary differences.
E.g. in C# extension methods can only extend a class, not override an existing method.
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()
Yes, we can write non-obvious code in any language, using all sorts of "confusing" techniques, especially when you do stuff dynamically.
class K: def __getattr__(self, attrname): if attrname == 'method': if __name__ == '__main__': raise AttributeError return something()
Regarding your example, you're only confused because you haven't take on board the fact that extension methods aren't interpreter-global, just module-global. Because it's new and unfamiliar. But we can do exactly the same thing, right now, with functions instead of methods, and you will find it trivially easy to diagnose the fault:
# file1.py from file2 import func # instead of "applying an extension method" from elsewhere, # import a function from elsewhere from extra_functions import g def spamify(obj): print(g(obj)) # works fine print(func(obj)) # fails
# file2.py def func(obj): return g(obj) # NameError
This example looks easy and not the least bit scary to you because you've been using Python for a while and it has become second nature to you. But you might remember back when you were a n00b, it probably confused you: why doesn't `g(obj)` work when you imported it? How weird and confusing! What do you mean, if I want to use g, I have to import it in each and every module where I want to use it? That's just dumb. Importing g once should make it available EVERYWHERE, right?
Been there, done that.
You learned about modules and namespaces, and why Python's design is *safer and better* than a single interpreter-global namespace, and now that doesn't confuse you one bit.
And if you were using Kotlin, or C#, or Swift, or any one of a number of other languages with extension methods, you would likewise learn that extension methods work in a similar fashion. Why does obj.method raise AttributeError from file2? *Obviously* its because you neglected to "apply the extension method", duh.
That's as obvious as neglecting to import something and getting a NameError. Maybe even more obvious, if your IDE or linter knows about extension methods.
And its *safer and better* than monkey-patching.
We have two people in this thread who know Kotlin and C#, at least one of them is a fan of the technique. Why don't we ask them how often this sort of error is a problem within the Kotlin and C# communities?
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.
Sure. Nobody says that extension methods are a Silver Bullet that cures all programming ills. Some things will need a monkey-patch.
Python is great because we have a rich toolbox of tools to choose from. To extend a class with more functionality at runtime, we can:
- monkey-patch the class;
- subclass it;
- single or multiple inheritance;
- or a virtual subclass;
- or use it as a mixin or a trait (with third-party library support);
- use delegation and composition;
- or any one of a number of Design Patterns;
- add methods onto the instance to override the methods on the class;
- swizzling (change the instance's class at runtime to change its behaviour);
- just write a function.
Have I missed anything? Probably. None of those techniques is a silver bullet, all of them have pros and cons. Not all of the techniques will work under all circumstances. We should use the simplest thing that will work, for whatever definition of "work" we need for that task.
Extension methods are just another tool in the tool box, good for some purposes, not so good for others.
That's why, despite its problems, I still think that monkey-patching is the cleaner option. It prevents objects from becoming context-dependent.
It might be a necessary thing under rather usual circumstances, but under the great bulk of circumstances, it is a bad thing.
Chris, here you are defending monkey-patching, not just as a necessary evil under some circumstances, but as a "cleaner" option, and then in your very next sentence:
And the Ruby community is starting to see the risks of monkey-patching.