On Wed, Jun 23, 2021 at 11:40 AM Steven D'Aprano email@example.com wrote:
On Tue, Jun 22, 2021 at 10:25:33PM +1000, Chris Angelico wrote:
If its a problem for getattr, it is a problem for dot syntax, because they are essentially the same thing.
Ahh but that is precisely the problem.
Is it? Don't be shy. Tell us what the problem is and why its a problem.
That's exactly what the rest of the post is about.
How is getattr defined?
The same as it is defined now, except with some minor tweaks to support extension methods.
Do those tweaks include reaching back into the module that called it? How magical will it be?
I thought you agreed that we didn't need to discuss implementation until we had decided on the desired semantics?
Let's just say it will be a well-defined, totally non-magical implementation (like everything else in Python) that manages to be equally efficient as regular attribute access. A high bar to set. Implementation issues may require us to dial that back a bit, or might even rule out the concept altogether, but let's start off by assuming the best and decide on the semantics first.
Semantics are *exactly* what I'm talking about.
This means that the getattr() function, being a perfectly straight-forward function, is not going to see any extension methods.
Does getattr see slots (both C-level and Python)? Yes. Does it see attributes in instance and class dicts? Yes. Does it see dynamic attributes that use `__getattr__`? Yes. Does it understand the descriptor protocol? Yes.
It does everything else dot notation does. Why wouldn't it see extension methods? (Apart from spite.)
The getattr builtin is just a public interface to whatever internal function or functions the interpreter uses to look up attributes.
Okay. Lemme give it to you *even more clearly* since the previous example didn't satisfy.
@extend(list) def in_order(self): return sorted(self)
def frob(stuff): return stuff.in_order()
from file1 import frob thing = [1, 5, 2] frob(thing) # == [1, 2, 5] def otherfrob(stuff): return stuff.in_order() otherfrob(thing) # AttributeError
Am I correct so far? The function imported from file1 has the extension method, the code in file2 does not. That's the entire point here, right?
Okay. Now, what if getattr is brought into the mix?
# file3.py @extend(list) def in_order(self): return sorted(self)
def fetch1(stuff, attr): if attr == "in_order": return stuff.in_order if attr == "unordered": return stuff.unordered return getattr(stuff, attr)
def fetch2(stuff, attr): return getattr(stuff, attr)
# file4.py from file3 import fetch1, fetch2 import random
@extend(list) def unordered(self): return random.shuffle(self[:])
def fetch3(stuff, attr): if attr == "in_order": return stuff.in_order if attr == "unordered": return stuff.unordered return getattr(stuff, attr)
def fetch4(stuff, attr): return getattr(stuff, attr)
thing = [1, 5, 2] fetch1(thing, "in_order")() fetch2(thing, "in_order")() fetch3(thing, "in_order")() fetch4(thing, "in_order")() fetch1(thing, "unordered")() fetch2(thing, "unordered")() fetch3(thing, "unordered")() fetch4(thing, "unordered")()
Okay. *NOW* which ones raise AttributeError, and which ones give the extension method? What exactly are the semantics of getattr? Is it a magical function that can reach back into the module that called it, or is it actually a function of its own? And if getattr is supposed to reach back into the other module, why shouldn't other functions be able to?
Please explain exactly what the semantics of getattr are, and exactly which modules it is supposed to be able to see. Remember, it is not a compiler construct or an operator. It is a function, and it lives in its own module (the builtins).
Not a rhetorical question: is that how it works in something like Swift, or Kotlin?
I have no idea. I'm just asking how you intend it to work in Python. If you want to cite other languages, go ahead, but I'm not assuming that they already have the solution, because they are different languages. Also not a rhetorical question: Is their getattr equivalent actually an operator or compiler construct, rather than being a regular function? Because if it is, then the entire problem doesn't exist.
And what about this?
f = functools.partial(getattr, stuff) f("in_order")
NOW which extension methods should apply? Those registered here? Those registered in the builtins? Those registered in functools?
partial is just a wrapper around its function argument, so that should behave *exactly* the same as `getattr(stuff, 'in_order')`.
So if it behaves exactly the same way that getattr would, then is it exactly the same as fetch2 and fetch4? If not, how is it different?
What about other functions implemented in C? If I write a C module that calls PyObject_GetAttr, does it behave as if dot notation were used in the module that called me, or does it use my module's extension methods?
You are handwaving getattr a crazy amount of magic here that basically amounts to "do what I want".
Yes, monkey-patching *is* cleaner, because the object is the same object no matter how you look it up.
Oh for heaven's sake, I'm not proposing changes to Python's object identity model! Please don't invent bogus objections that have no basis in the proposal. The id() function and `is` operator will work exactly the same as they do now. Classes with extension methods remain the same object. The only difference is in attribute lookups.
You know what I mean. Stop being obtuse. The object has notably different behaviour depending on where you are when you look at it. In every other way in Python, an object is what it is regardless of who's asking - but now this is proposing changing that.
(Oh, and another wrinkle, although a small one: Code objects would need to keep track of their modules.
Would they? I'm not seeing the connection between code objects used by functions and attribute lookups. Perhaps they would, but it's not clear to me what implementation you are thinking of when you make this statement.
Attribute lookups are done by bytecode, which lives in code objects. You can execute a code object without an associated function, and you can have functions in different modules associated with the same code object. When you run that bytecode, which set of extension methods would it look up? The sanest approach I can think of is that the code object would remember which module it was created in (which is broadly the same as the way PEP 479 does things - although since that's a binary state, it simply sets one flag on the code object).
`getattr` doesn't even have a `__code__` attribute, and neither do partial objects.
Builtin functions don't have bytecode, they have C code, but they'd need an equivalent. Partial objects have a func attribute, which would be where you'd go looking for the code (either a code object or C code). None of this changes the fact that code objects still would need to know their modules.