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?

Good question.

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":

    uses spam

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

    SpammySpam().spam

and 

    Advertising().spam

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.


-- 
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/JH76324NCT2NH7FKPSXVA2VTEIODQQRZ/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to