Hello, On Thu, 17 Dec 2020 12:46:17 +1300 Greg Ewing <greg.ew...@canterbury.ac.nz> wrote:
> On 17/12/20 8:16 am, Paul Sokolovsky wrote: > > With all the above in mind, Python3.7, in a strange twist of fate, > > and without much ado, has acquired a new operator: the method call, > > ".()". > > > CPython3.6 and below didn't have ".()" operator, and compiled it as > > "attr access" + "function call", but CPython3.7 got the new > > operator, and compiles it as such (the single operator). > > Um, no. There is no new operator. You're reading far more into the > bytecode change than is actually there. So, here we would need to remember what construes "a good scientific theory". It's something which explain sufficiently wide array of phenomena with sufficiently concise premises, predicts effects of phenomena, and allows to use all that in some way which is "useful". Henceforth, the theory of the call operator was presented. And if we look at the actual code, then "a.b" is compiled into LOAD_ATTR, and "a.b()" into LOAD_METHOD, *exactly* as the theory predicts. It also makes it clear what is missing from the middle-layer (abstract syntax) to capture the needed effect - literally, MethodCall AST node. It also explains what's the meaning (and natural effect) of "(a.b)()". And that explanation agrees with an intuitive meaning of the use of parens for grouping. That meaning being "do stuff in parens before", and indeed, theory tells us, that "(a.b)()" should naturally be implemented by LOAD_ATTR, followed by CALL_FUNCTION. Which is again agrees with how some implementations do that. Given all that, "unlearning" a concept of a method call operator may be not easier than "unlearning" a concept of natural numbers (or real numbers, or transcendental numbers, or complex numbers). Please be my guest. > > So, why CPython3.7+ still compiles "(a.b)()" using LOAD_METHOD. > > Because a.b() and (a.b)() have the same semantics, *by definition*. > That definition has NOT changed. Because they have the same > semantics, there is no need to generate different bytecode for them. By *a* particular definition. The theory is above a particular definition. It explains how "a.b" vs "a.b()" should be compiled, and indeed, they that way. It also explains how "(a.b)()" should be compiled, and the fact that particular implementations "optimize" it doesn't invalidate the theory in any way. And yeah, as soon as we, umm, adjust the definition, the theory gets useful to explain what may need to be changed in the codegeneration. > > But still, are there Python implementations which compile "(a.b)()" > > faithfully, with its baseline semantic meaning? Of course there're. > > E.g., MicroPython-derived Python implementations compile it in the > > full accordance with the theory presented here: > > All that means is that Micropython is missing a potential > optimisation. This is probably a side effect of the way its > parser and code generator work, rather than a conscious decision. Good nailing down! But it's the same for CPython. CPython compiles "(a.b)()" using LOAD_METHOD not because it consciously "optimizes" it, but simply because it's *unable* to represent the difference between "a.b()" and "(a.b)()". More specific explanation: 1. MicroPython compiler is CST (concrete syntax tree) based, so it sees all those parens. (And yes, it has to do silly things to not miss various optimizations, and still misses a lot.) 2. CPython compiler is AST (abstract syntax tree) based, and as the current ASDL definition (https://github.com/python/cpython/blob/master/Parser/Python.asdl) misses the proper MethodCall node, it conflates "a.b()" and "(a.b)()" together. So, the proper way to address the issue would be to add explicit MethodCall node. So far (for Pycopy), I don't dare for such a step, instead having a new "is_parenform" attribute on existing nodes, telling that corresponding node was parens-wrapped in the surface syntax. (And I had it before, as it's needed to e.g. parse generator expressions with a recursive-decent parser.) > Now, it's quite possible to imagine a language in which > a.b() and (a.b)() mean different things. Not only that! It's even possible to imagine Python dialect where "a.b" and "a.b()" would mean *exactly* what they are - first is attribute access, second is method call, no exceptions allowed. But then the question will arise how to call a callable stored in an attributed. So tell me, Greg (first thing which comes to your mind, for as long as it's "a)" or "b)" please) what do you like better: a) (a.b)() syntax b) apply() being resurrected > Does anyone remember > Prothon? (It's a language someone was trying to design a while > back that was similar to Python but based on prototypes > instead of classes.) I don't remember it. Turned out, google barely remembers it, and I had to give it a bunch of clarifying questions and "do you really mean that?" answer before I got to e.g. http://wiki.c2.com/?ProthonLanguage > A feature of Prothon was that a.b() and t = a.b; t() would do > quite different things (one would pass a self argument and the > other wouldn't). > > I considered that a bad thing. I *like* the fact that in Python > I can use a.b to get a bound method object and call it later, > with the same effect as if I'd called it directly. > > I wouldn't want that to change. Fortunately, it never will, > because changing it now would break huge amounts of code. Of course it never will. It == the current Python's semantic mode. Instead, new modes will capitalize on the newly discovered features. One such mode, humbly named "the Strict Mode", was presented here on the list recently. But that was only the part 1 of strict mode, . We just discussed the pillar #1 of the strict mode, part 2. For that pillar being the separation between methods and attributes. But you'll ask "perhaps we can separate them, but how can we *clearly* separate those, so there was no ambiguity?". Indeed, that would be pillar #2. It is inspired (retroactively of course, as I had that idea for many years) by feedback received during discussion of the idea of block-scoped variables. Some people immediately responded that they want shadowing to be disallowed (e.g. https://mail.python.org/archives/list/python-ideas@python.org/message/3IKFBQ5NZ2X7RARMMJORM4V7GSVV5IQG/). Of course, it doesn't make any sense to disallow shadowing of block-scoped local variables, for that bringing no theoretical or implementation benefits (and practical benefits are addressed by linters or opt-in warnings). But those people were absolutely right - there're places in Python where shadowing is truly harmful. So, their wish is granted, but in the good tradition of wish-granting, not where and not in a way they wanted. For the strict mode, part 2, pillar #2 saying: "It's not allowed to shadow a method with an attribute". And combined, parts 1 and 2 allow to optimize namespace lookups in Python, where part 1 deals with module/class namespaces, and part 2 with object namespaces. What's interesting, that part 2, just like part 1, of the strict mode doesn't really make any "revolutionary" changes. It just exposes, emphasizes, and makes consistent, the properties Python language already has. Ain't that cute? Do you spot any issues, Greg? > -- > Greg [] -- Best regards, Paul mailto:pmis...@gmail.com _______________________________________________ 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/PF42DP2IYYRFUJMYCJ2NDRBXU57VNFEH/ Code of Conduct: http://python.org/psf/codeofconduct/