On Tue, Jun 22, 2021 at 03:56:00AM +1000, Chris Angelico wrote:
I'm actually not concerned so much with the performance as the confusion. What exactly does the registration apply to?
Extension methods have four steps:
- you write a method;
- declare which class it extends;
- the caller declares that they want to use extensions;
- and they get looked up at runtime (because we can't do static lookups).
The first two can go together. I might write a module "spam.py". Borrowing a mix of Kotlin and/or C# syntax, maybe I write:
def list.head(self, arg): ...
def list.tail(self, arg): ...
or maybe we have a decorator:
@extends(list) def head(self, arg): ...
The third step happens at the caller site. Using the C# keyword, you might write this in your module "stuff.py":
or maybe there's a way to do it with the import keyword:
# could be confused for `from spam import extensions`? import extensions from spam
from functools import extension_methods import spam extension_methods.load_from(spam)
whatever it takes. Depends on how much of this needs to be baked into the interpreter.
Fourth step is that you go ahead and use lists as normal. Whether you use getattr or dot syntax, any extension methods defined in spam.py will show up, as if they were actual list methods.
hasattr(, 'head') # returns True list.tail # returns the spam.tail function object (unbound method)
They're not monkey-patched: other modules don't see that.
And suppose you have a series of extension methods that you want to make use of in several modules in your project, how can you refactor a bunch of method registration calls so you can apply them equally in multiple modules? We don't need an implementation yet - but we need clear semantics.
I put the extension modules in one library. That may not literally require me to put their definitions in a single .py file, I should be able to use a package and import extension methods from modules the same as any other object. But for ease of use for the caller, I probably want to make all my related extension methods usable from a single place.
Then you, the caller, import/use them from each of your modules where you want to use them:
# stuff.py uses spam
# things.py uses spam
And in modules where you don't want to use them, you just don't use them.
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.
And that's a very very big "if". Monkey-patching can be used for unittest mocking, but that won't work here. Monkey-patching can be used to fix bugs in someone else's code, but that only works here if *your* code is in a single module, or you reapply the monkey-patch in every module. I'm really not seeing a lot of value in the proposal.
LINQ is a pretty major part of the C# ecosystem. I think that proves the value of extension methods :-)
I know we're not really comparing apples with apples, Python's trade-offs are not the same as C#'s trade-offs. But Ruby is a dynamic language like Python, and they use monkey-patching all the time, proving the value of being able to extend classes without subclassing them.
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.
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.
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.