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?
This is clearly wrong. The local override for the LOAD_ATTR opcode should NOT apply to proxy methods except where explicitly requested. Also sometimes you are supposed to call the dunder directly, like in the above example. It's not *bad* to do it if you know what you're doing.
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. Your extension methods shouldn't propagate to proxy objects.
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. 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. 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!)
On 2021-06-22 6:57 a.m., Steven D'Aprano wrote:
I'm sorry Soni, I don't understand what you are arguing here. See below.
On Mon, Jun 21, 2021 at 10:09:17PM -0300, Soni L. wrote:
On 2021-06-21 9:39 p.m., Steven D'Aprano wrote:
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.
Python is a dynamic language. Maybe you're using hasattr/getattr to forward something from A to B. If "other modules don't see that" then this must work as if there were no extension methods in place.
What's "forward something from A to B" mean? What are A and B?
If "this" (method lookups) "must work as if there were no extension methods in place" then extension methods are a no-op and are pointless. You write an extension method, register it as applying to a type, the caller opts-in to use it, and then... nothing happens, because it "must work as if there were no extension methods in place".
Surely that isn't what you actually want to happen. But if not, I have no idea what you mean.
The whole point of extension methods is that once the caller opts in to use them, method look ups (and that includes hasattr and getattr) must work as if the extension methods **are in place**.
The must be no semantic difference between:
regardless of whether `method` is a regular method or an extension method.
So you actually wouldn't want the local load_attr override to apply to those. If you did... well, just call the override directly.
I have no idea what that means. What is "the local load_attr override"?
If the override was called __opcode_load_attr_impl__ you'd just call __opcode_load_attr_impl__ directly instead of going through getattr.
As a general rule, you should not be calling dunders directly.
You seem to have missed the point that extension methods are intended as a mechanism to **extend a type** by giving it new methods on an opt-in basis. I want to call them "virtual methods" except that would add confusion regarding virtual subclasses and ABCs etc.
Maybe you need to read the Kotlin docs:
and the C# docs:
Wikipedia also has a broad overview from a language-agnostic perspective:
In particular note these comments:
# Kotlin "Such functions are available for calling in the usual way as if they were methods of the original class."
# C# "Extension methods are only in scope when you explicitly import the namespace into your source code with a using directive."
Both C# and Kotlin are statically typed languages, and Python is not, but we ought to aim to minimise the differences in semantics. Aside from extension methods being resolved at runtime instead of at compile time, the behaviour ought to be as close as possible.
Just as single dispatch in Python is resolved dynamically, but aims to behave as close as possible to single dispatch in statically typed languages.
Another important quote:
"Because extension methods are called by using instance method syntax, no special knowledge is required to use them from client code. To enable extension methods for a particular type, just add a `using` directive for the namespace in which the methods are defined."
"No special knowledge is required" implies that, aside from the opt-in step itself, extension methods must behave precisely the same as regular methods. That means they will be accessible as bound methods on the instance:
and unbound methods (functions) on the type:
and using dynamic lookup:
and they will fully participate in inheritance heirarchies if you have opted in to use them.
There needs to be an escape hatch for this.
The escape hatch is to *not* opt-in to the extension method. If the caller doesn't opt-in, they don't get the extension methods.
That is the critical difference between extension methods and monkey- patching the type. Monkey-patching effects everyone. Extension methods have to be opt-in.
Or you *could* have getattr be special (called by load_attr) and overridable, and builtins.getattr be the escape hatch, but nobody would like that.
Huh? Unless you have shadowed getattr with a module-level function, getattr *is* builtins.getattr.