On Tue, Jun 22, 2021 at 08:44:56AM -0300, Soni L. wrote:
Oh this is a long one.
Hypothetically, let's say you have a proxy object:
class Foo: def __getattribute__(self, thing): return getattr(super().__getattribute__(self, "proxied"), thing)
Should this really include extension methods into it by default?
By default? Absolutely not. Extension methods are opt-in.
This is clearly wrong.
What is clearly wrong? Your question? A "yes" answer? A "no" answer? Your proxy object?
Soni, and Chris, you seem to be responding as if extension methods are clearly, obviously and self-evidently a stupid idea. Let me remind you that at least ten languages (C#, Java, Typescript, Oxygene, Ruby, Smalltalk, Kotlin, Dart, VB.NET and Swift) support it. Whatever the pros and cons of the technique, it is not self-evidently wrong or stupid.
The local override for the LOAD_ATTR opcode should NOT apply to proxy methods except where explicitly requested.
Let me ask you this:
- should proxy objects really include `__slots__` by default?
- should they really include dynamic attributes generated by `__getattr__`?
- should they include attributes in the inheritence hierarchy?
- why should extension methods be any different?
Let's step back from extension methods and consider a similar technique, the dreaded monkey-patch. If I extend a class by monkey-patching it with a new method:
import library library.Klass.method = patch_method
would you expect that (by default) the patched method are invisible to proxies of Klass? Would you expect there to be a way to "opt-out" of proxying that method?
I hope that your answers are "No, and no", because if either answer is "yes", you will be very disappointed in Python.
Why should extension methods be different from any other method? Let's go through the list of methods which are all treated the same:
- methods defined on the class; - methods defined on a superclass or mixin; - methods added onto the instance; - methods created dynamically by `__getattr__`.
(Did I miss any?)
And the list of those which are handled differently, with ways to opt-out of seeing them:
- ... um... er...
Have I missed any?
The point is that the caller using your proxy object should opt-in to the extension methods, rather than break with no way to opt-out of them.
You opt-out by not opting in.
Your extension methods shouldn't propagate to proxy objects.
Fundamentally, your proxy object is just doing attribute lookups on another object. If you have a proxy to an instance `obj`, there should be no difference in behaviour between extension methods and regular methods. If `obj.method` succeeds, so should `proxy.method`, because that's what proxies do.
The origin of obj.method should not make any difference. I'm sorry to have to keep harping on this, but it doesn't matter to the proxy whether the method exists in the instance `__dict__`, or the class `__dict__`, or `__slots__`, or a superclass, or is dynamically generated by `__getattr__`. A method is a method.
Extension methods are methods.
To go even further, should all your class definitions that happen to extend a class with in-scope extension methods automatically gain those extension methods? Because with actual extension methods, that doesn't happen.
We might want to follow the state of the art here, assuming there was consensus in other languages about inheriting extension methods. But I would expect this behaviour:
# --- extensions.py library ---
@extends(list) def flatten(self): ...
# --- module A.py ---
uses extensions # opt-in to use the extension method
class MyListA(list): pass
MyListA.flatten # inherits from list
# --- module B.py ---
class MyListB(list): pass
MyListB.flatten # raises AttributeError
However, there may be factors I haven't considered.
You can have class MyList(list): pass and other callers would not get MyList.flatten even with you being able to use MyList.flatten locally.
Extension methods are more like Rust traits than inheritance-based OOP.
I understand that Rust doesn't support inheritance at all, and that Rust traits are more like what everyone else calls "interfaces".
Also note that they use instance method syntax, but no other. That is they apply to LOAD_ATTR opcodes but should not apply to getattr! (Indeed, reflection in C#/Kotlin doesn't see the extension methods!)
Okay, that's a good data point. The question is, why doesn't reflection see the extension methods? That will help us decide whether that's a limitation of reflection in those languages, or a deliberate design feature we should follow.
A brief search suggests that people using C# do want to access extension methods via reflection, and that there are ways to do so: