Re: [Python-ideas] Add .= as a method return value assignment operator

Hi Jasper, This seems like a great idea! It looks so much cleaner, too. Would there be a dunder method handling this? Or since it's explicitly just a syntax for "obj = obj.method()" is that not necessary? My only qualm is that this might get PHP users confused; that's really not an issue, though, since Python is not PHP. Anyway, I fully support this idea. Sincerely, Ken Hilton;

Absolutely -1 on this. Consider the following example: def encode(s, *args): """Force UTF 8 no matter what!""" return s.encode('utf8') text = "Hello, there!" text .= encode('latin1') Do you see how this creates an ambiguous situation? Implicit attribute lookup like this is really confusing. It reminds me of the old `with` construct in javascript that is basically forbidden now, because it created the same situation. On Thu, Sep 27, 2018 at 6:49 AM Ken Hilton <kenlhilton@gmail.com> wrote:

Do you see how this creates an ambiguous situation?
Name shadowing could create such ambiguous situations anyways even without the new operator?
In my opinion, it goes against the notion that explicit is better than implicit.
By that logic all operation-assign operations violate that rule (e.g. "-=", "+=" etc.). I honestly don't feel how this feels any less explicit that what already exists in the language.
Assumes that every function returns a value or perhaps implies purity.
So does `obj = obj.func(args)` or I've misunderstood you? I like the operator, so +1, though I feel it could be even more useful if it allowed accessing class members, `obj = obj.id` being equivalent to `obj .= id`, what do you think? On Thu, Sep 27, 2018 at 8:05 PM MRAB <python@mrabarnett.plus.com> wrote:

On Thu, Sep 27, 2018 at 10:44:38PM +0300, Taavi Eomäe wrote:
How? Can you give an example? Normally, there is no way for a bare name to shadow a dotted name, or vice versa, since the dotted name always needs to be fully qualified. In the example below, we can talk about "text.encode" which is always the method, or "encode", which is always the function and never the method. I agree that this adds ambiguity where we can't be sure whether text .= encode('utf-8') is referring to the function or the method. We can infer that it *ought* to be the method, and maybe even add a rule to force that, but this works only for the simple cases. It is risky and error prone in the hard cases. Think about a more complex assignment: text .= encode(spam) + str(eggs) This could mean any of: text.encode(spam) + text.str(eggs) text.encode(spam) + str(eggs) encode(spam) + text.str(eggs) encode(spam) + str(eggs) In a statically typed language, the compiler could work out what is needed at compile-time (it knows that text.str doesn't exist) and either resolve any such ambiguities or refuse to compile. But in a dynamic language like Python, you can't tell whether text.str exists or not until you try it. So this proposal suffers from the same problems as Pascal-style "with" blocks, which are a FAQ: https://docs.python.org/3/faq/design.html#why-doesn-t-python-have-a-with-sta... [...]
-- Steve

On Thu, Sep 27, 2018 at 09:47:42PM -0400, James Lu wrote:
We would be innundated with questions and bug reports complaining that # earlier... argument = something() # later on myobj = MyClass() myobj .= method(argument) fails with AttributeError: 'MyClass' object has no attribute 'argument' and people would be right to complain. That rule would mean you couldn't use builtins or module-level names on the right hand side of the .= operator. I call that a huge misfeature.
This behavior prevents the worst mistakes and makes it very clear what is happening.
Far from preventing mistakes, it would cause them, whenever somebody intended to use a builtin or module-level object, and accidently got a completely different attribute of the target object. -- Steve

Steven D'Aprano wrote:
Think about a more complex assignment:
text .= encode(spam) + str(eggs)
I think the only sane thing would be to disallow that, and require the RHS to have the form of a function call, which is always interpreted as a method of the LHS. -- Greg

On Fri, Sep 28, 2018 at 05:34:58PM +1200, Greg Ewing wrote:
You obviously have a different idea of what is "sane" than I do :-) But okay, so we cripple the RHS so that it can only be a single method call. So useful things like these are out: target .= method(arg) or default target .= foo(arg) if condition else bar(arg) and even target .= method(args) + 1 making the syntax pure sugar for target = target.method(args) and absolutely nothing else. I think that's the sort of thing which gives syntactic sugar a bad name. The one positive I can see is that if the target is a compound expression, it could be evaluated once only: spam[0](x, y, z).eggs['cheese'].aardvark .= method(args) I suppose if I wrote a lot of code like that, aside from (probably?) violating the Law of Demeter, I might like this syntax because it avoids repeating a long compound target. -- Steve

Summary: I recast an example in a more abstract form. Steve D'Aprano wrote:
Think about a more complex assignment: text .= encode(spam) + str(eggs)
I find this example instructive. I hope the following is also instructive: $ python3
To me this shows that LHS += RHS works as follows: 1. Evaluate the LHS (as an assignable object). 2. Evaluate the RHS (as a value). and then some more steps, not covered in my example. As syntax the compound symbols '+=' and '.=' are similar. But in semantics, '+=' does and '.=' does not have an evaluation of the RHS as an expression. This is, in abstract terms, the origin of Steve's example. Someone else has noted that '+=' and its variants are focused on numeric operations, such as addition. This shows, to me, that the simplification provided by use cases such as text = text.replace("foo","bar") has to be compared to the complexity introduced by text .= encode(spam) + str(eggs) In other words, I've restated Steve's example, in a more abstract form. I hope it helps to have another way to look at this example. Finally, I note
-- Jonathan

On Fri, Sep 28, 2018 at 6:56 PM Jonathan Fine <jfine2358@gmail.com> wrote:
? Yes? That's what 2 ** 3 is, so that's what I would expect. All other augmented assignment operators take an assignment target on the left and a (single) value on the right. ChrisA

I don’t really like this proposal for the same reasons you do I think but let’s play devils advocate for a while... I would then suggest something more readable like: foo as x = x.bar or x.something_else or even_another_thing This avoids the iffy hanging dot while allowing more complex and readable statements. It also expands trivially to tuple unpacking which the .= syntax does not. / Anders

On 2018-09-27 03:48, Ken Hilton wrote:
I'm opposed to this idea, precisely because there's no way to make a dunder for it. Exiting augmented assignment operators work like this: "LHS op= RHS" means "LHS = LHS.__iop__(RHS)" (where iop is iadd, isub, etc.). This can't work for a dot operator. If "LHS .= RHS" is supposed to mean "LHS = LHS.RHS", then what is the argument that is going to be passed to the dunder method? The proposed .= syntax is using the RHS as the *name* of the attribute to be looked up, but for all existing augmented assignments, the RHS is the *value* to be operated on. As others pointed out elsewhere in the thread, the problem is compounded if there are multiple terms in the RHS. What does "this .= that + other" mean? What would be the argument passed to the dunder function? Is the name of the relevant attribute supposed to be taken from "that" or "other" or from the result of evaluating "that + other"? I like the consistency of the existing augmented assignment operations; they are just all syntactic sugar for the same pattern: LHS = LHS.__iop__(RHS) . I'm opposed to the creation of things that look like augmented assignment but don't follow the same pattern, which is what this proposal does. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Le 27/09/2018 à 12:48, Ken Hilton a écrit :
What would the following evaluate to? a .= b + c(d) 1: a = a.b + a.c(a.d) # everything is prepended an "a." it means we dn't have access to any external elements, making the functionality only useful in a few cases 2: a = a.b + a.c(d) # every first level element (if that means something) is prepended an "a." We still lose some of the ability to access anything outside of `a`, but a bit less than in #1. The effort to understand the line as grown a bit, though. 3: a = a.(b + c(d)) # everything is evaluated, and an "a." is prepended to that result (the same way `a *= 2 + 3` is equivalent to `a *= 5`) I believe in most cases, this wouldn't mean anything to evaluate `b + c(d)` on their own, and expect a return that can be used as an attribute of `a`. 4: a = a.b + c(d) # "a." is prepended to the first element after the `=` It is probably quite easy to read and understand, but it removes the transitivity of the operators we have on the right, and is a bit limiting. 5: SyntaxError: Can only use the [whatever the name] augmented operator with a single expression Why not, it's a bit limiting, but is clear enough to me. Maybe, a simpler thing to do for this problem would be to make something like this: a = .b(5) + c(.d) + 3 being the equivalent of a = a.b(5) + c(a.d) + 3 I don't see any ambiguity anymore, it shortens the code a lot, and I guess it wouldn't be hard for the compiler to recompose the line as a first parsing step, and create the same AST with both syntaxes.

Absolutely -1 on this. Consider the following example: def encode(s, *args): """Force UTF 8 no matter what!""" return s.encode('utf8') text = "Hello, there!" text .= encode('latin1') Do you see how this creates an ambiguous situation? Implicit attribute lookup like this is really confusing. It reminds me of the old `with` construct in javascript that is basically forbidden now, because it created the same situation. On Thu, Sep 27, 2018 at 6:49 AM Ken Hilton <kenlhilton@gmail.com> wrote:

Do you see how this creates an ambiguous situation?
Name shadowing could create such ambiguous situations anyways even without the new operator?
In my opinion, it goes against the notion that explicit is better than implicit.
By that logic all operation-assign operations violate that rule (e.g. "-=", "+=" etc.). I honestly don't feel how this feels any less explicit that what already exists in the language.
Assumes that every function returns a value or perhaps implies purity.
So does `obj = obj.func(args)` or I've misunderstood you? I like the operator, so +1, though I feel it could be even more useful if it allowed accessing class members, `obj = obj.id` being equivalent to `obj .= id`, what do you think? On Thu, Sep 27, 2018 at 8:05 PM MRAB <python@mrabarnett.plus.com> wrote:

On Thu, Sep 27, 2018 at 10:44:38PM +0300, Taavi Eomäe wrote:
How? Can you give an example? Normally, there is no way for a bare name to shadow a dotted name, or vice versa, since the dotted name always needs to be fully qualified. In the example below, we can talk about "text.encode" which is always the method, or "encode", which is always the function and never the method. I agree that this adds ambiguity where we can't be sure whether text .= encode('utf-8') is referring to the function or the method. We can infer that it *ought* to be the method, and maybe even add a rule to force that, but this works only for the simple cases. It is risky and error prone in the hard cases. Think about a more complex assignment: text .= encode(spam) + str(eggs) This could mean any of: text.encode(spam) + text.str(eggs) text.encode(spam) + str(eggs) encode(spam) + text.str(eggs) encode(spam) + str(eggs) In a statically typed language, the compiler could work out what is needed at compile-time (it knows that text.str doesn't exist) and either resolve any such ambiguities or refuse to compile. But in a dynamic language like Python, you can't tell whether text.str exists or not until you try it. So this proposal suffers from the same problems as Pascal-style "with" blocks, which are a FAQ: https://docs.python.org/3/faq/design.html#why-doesn-t-python-have-a-with-sta... [...]
-- Steve

On Thu, Sep 27, 2018 at 09:47:42PM -0400, James Lu wrote:
We would be innundated with questions and bug reports complaining that # earlier... argument = something() # later on myobj = MyClass() myobj .= method(argument) fails with AttributeError: 'MyClass' object has no attribute 'argument' and people would be right to complain. That rule would mean you couldn't use builtins or module-level names on the right hand side of the .= operator. I call that a huge misfeature.
This behavior prevents the worst mistakes and makes it very clear what is happening.
Far from preventing mistakes, it would cause them, whenever somebody intended to use a builtin or module-level object, and accidently got a completely different attribute of the target object. -- Steve

Steven D'Aprano wrote:
Think about a more complex assignment:
text .= encode(spam) + str(eggs)
I think the only sane thing would be to disallow that, and require the RHS to have the form of a function call, which is always interpreted as a method of the LHS. -- Greg

On Fri, Sep 28, 2018 at 05:34:58PM +1200, Greg Ewing wrote:
You obviously have a different idea of what is "sane" than I do :-) But okay, so we cripple the RHS so that it can only be a single method call. So useful things like these are out: target .= method(arg) or default target .= foo(arg) if condition else bar(arg) and even target .= method(args) + 1 making the syntax pure sugar for target = target.method(args) and absolutely nothing else. I think that's the sort of thing which gives syntactic sugar a bad name. The one positive I can see is that if the target is a compound expression, it could be evaluated once only: spam[0](x, y, z).eggs['cheese'].aardvark .= method(args) I suppose if I wrote a lot of code like that, aside from (probably?) violating the Law of Demeter, I might like this syntax because it avoids repeating a long compound target. -- Steve

Summary: I recast an example in a more abstract form. Steve D'Aprano wrote:
Think about a more complex assignment: text .= encode(spam) + str(eggs)
I find this example instructive. I hope the following is also instructive: $ python3
To me this shows that LHS += RHS works as follows: 1. Evaluate the LHS (as an assignable object). 2. Evaluate the RHS (as a value). and then some more steps, not covered in my example. As syntax the compound symbols '+=' and '.=' are similar. But in semantics, '+=' does and '.=' does not have an evaluation of the RHS as an expression. This is, in abstract terms, the origin of Steve's example. Someone else has noted that '+=' and its variants are focused on numeric operations, such as addition. This shows, to me, that the simplification provided by use cases such as text = text.replace("foo","bar") has to be compared to the complexity introduced by text .= encode(spam) + str(eggs) In other words, I've restated Steve's example, in a more abstract form. I hope it helps to have another way to look at this example. Finally, I note
-- Jonathan

On Fri, Sep 28, 2018 at 6:56 PM Jonathan Fine <jfine2358@gmail.com> wrote:
? Yes? That's what 2 ** 3 is, so that's what I would expect. All other augmented assignment operators take an assignment target on the left and a (single) value on the right. ChrisA

I don’t really like this proposal for the same reasons you do I think but let’s play devils advocate for a while... I would then suggest something more readable like: foo as x = x.bar or x.something_else or even_another_thing This avoids the iffy hanging dot while allowing more complex and readable statements. It also expands trivially to tuple unpacking which the .= syntax does not. / Anders

On 2018-09-27 03:48, Ken Hilton wrote:
I'm opposed to this idea, precisely because there's no way to make a dunder for it. Exiting augmented assignment operators work like this: "LHS op= RHS" means "LHS = LHS.__iop__(RHS)" (where iop is iadd, isub, etc.). This can't work for a dot operator. If "LHS .= RHS" is supposed to mean "LHS = LHS.RHS", then what is the argument that is going to be passed to the dunder method? The proposed .= syntax is using the RHS as the *name* of the attribute to be looked up, but for all existing augmented assignments, the RHS is the *value* to be operated on. As others pointed out elsewhere in the thread, the problem is compounded if there are multiple terms in the RHS. What does "this .= that + other" mean? What would be the argument passed to the dunder function? Is the name of the relevant attribute supposed to be taken from "that" or "other" or from the result of evaluating "that + other"? I like the consistency of the existing augmented assignment operations; they are just all syntactic sugar for the same pattern: LHS = LHS.__iop__(RHS) . I'm opposed to the creation of things that look like augmented assignment but don't follow the same pattern, which is what this proposal does. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

Le 27/09/2018 à 12:48, Ken Hilton a écrit :
What would the following evaluate to? a .= b + c(d) 1: a = a.b + a.c(a.d) # everything is prepended an "a." it means we dn't have access to any external elements, making the functionality only useful in a few cases 2: a = a.b + a.c(d) # every first level element (if that means something) is prepended an "a." We still lose some of the ability to access anything outside of `a`, but a bit less than in #1. The effort to understand the line as grown a bit, though. 3: a = a.(b + c(d)) # everything is evaluated, and an "a." is prepended to that result (the same way `a *= 2 + 3` is equivalent to `a *= 5`) I believe in most cases, this wouldn't mean anything to evaluate `b + c(d)` on their own, and expect a return that can be used as an attribute of `a`. 4: a = a.b + c(d) # "a." is prepended to the first element after the `=` It is probably quite easy to read and understand, but it removes the transitivity of the operators we have on the right, and is a bit limiting. 5: SyntaxError: Can only use the [whatever the name] augmented operator with a single expression Why not, it's a bit limiting, but is clear enough to me. Maybe, a simpler thing to do for this problem would be to make something like this: a = .b(5) + c(.d) + 3 being the equivalent of a = a.b(5) + c(a.d) + 3 I don't see any ambiguity anymore, it shortens the code a lot, and I guess it wouldn't be hard for the compiler to recompose the line as a first parsing step, and create the same AST with both syntaxes.
participants (14)
-
Anders Hovmöller
-
Brendan Barnwell
-
Brice Parent
-
Calvin Spealman
-
Chris Angelico
-
Greg Ewing
-
James Lu
-
Jasper Rebane
-
Jonathan Fine
-
Ken Hilton
-
MRAB
-
Stelios Tymvios
-
Steven D'Aprano
-
Taavi Eomäe