4 ms·
Not so. Many things change the semantics, otherwise high level languages would just be glorified macros. For example, import mod is NOT defined as mod = eva
by akvadrako 8y ago
Not so. Many things change the semantics, otherwise high level languages would just be glorified macros.
For example, import mod is NOT defined as
mod = eval(open("mod.py").read())
but involves abstract load module operation, which is dependant on the environment.
That's why := is just syntactic sugar; there are no new semantics.
- junke 8y agoHonestly curious, how do you expand a "x := y" expression into an expression that returns y and affects x as a side-effect?
- akvadrako 8y agoI'm not sure exactly, but my point is that a program transformation can do it. However, no transformation can change an import to other operations while perserving semantics. Just for fun, this seems to work: (locals().pop('x', None), locals().setdefault('x', y))[1]
- yorwba 8y agoThat only works at top-level scope, where locals() == globals(). Inside a function, changing the dict returned by locals() doesn't actually change the variable.
- saltcured 8y agoDoes syntactic sugar have to be that strictly limited? If we are allowed to manipulate the statement block containing the sugared expression, we could define a trivial helper function which performs the 'nonlocal' side-effect and returns the value. Then, replace the "x := y" with "helper(y)".
- chriswarbo 8y ago> there are no new semantics I don't think that's right; what expression/statement is `x := y` equivalent to? I'm thinking in particular about using mutable collections to emulate assignment in a lambda, e.g. >>> counter = (lambda c: lambda: (c.append(c.pop() + 1), c[0])[1])([0]) >>> counter() 1 >>> counter() 2 >>> counter() 3 It looks like this could now be done as: >>> counter = (lambda c: lambda: (c := c + 1))(0) Yet the semantics here are very different: one is pushing and popping the contents of a list, without changing any variable bindings (`c` always points to the same list, but that list's contents changes); the other has no list, no pushing/popping, and does change the variable bindings (`c` keeps pointing to different integers). Maybe it's equivalent to using a `=` statement, but statements are forbidden inside lambdas. Maybe the lambdas are equivalent to `def ...` functions, but what would their names be? Even if we made the outer one `def counter(c)...` the resulting value would have a different `func_name` (`counter` versus `<lambda>`). Even the `if` examples that are scattered around this page don't seem to have an equivalent. For example: if (x := foo() is not None): do_something() We can't "desugar" this, e.g. to something like the following: x = foo() if x is not None: do_something The reason is that we're changing the point at which the binding takes place. For example, Python guarantees to evaluate the elements of a tuple in left to right order (which we exploited in the above push/pop example). That means we could write: if (sys.stdout.write(x), x := foo() is not None)[1]: do_something This will print the current value of `x`, then update `x` to the return value of `foo()`. I can't think of a way to desugar this which preserves the semantics. For example, using the incorrect method from above: x = foo() if (sys.stdout.write(x), x is not None)[1]: do_something This isn't equivalent, since it will print the new value of `x`. Maybe we could float the `write` call out of the condition too, but what about something like: if foo(x) and (x := bar()): do_something We would have to perform `foo(x)` with the old value of `x`, store the result somewhere (a fresh temporary variable?), perform the `x = bar()` assignment, reconstruct the condition using the temporary variable and the new value of `x`, then `del` the temporary variable (in case `do_something` makes use of `locals()`). PS: I think this `:=` is a good thing, and writing the above examples just reminded me how infuriating it is when high-level languages distinguish between statements and expressions, rather than having everything be an expression!
- akvadrako 8y agoIt seems like your convinced yourself it is just sugar by the end of your post. You need to use a temporary variable but then your example is easy.
- chriswarbo 8y agoNo idea why you think it seems that way. Using a temporary variable is not "just sugar", it's a big change in semantics! Take a more familiar example: x, y = (y, x) Let's pretend that this is "just sugar" for using a temporary variable. What would the desugared version look like? As a first guess, how about: z = (y, x) x = z[0] y = z[1] del(z) This seems fine, but it's wrong. For example, it would break the following code (since `z` would get clobbered): z = "hello world" x, y = (y, x) print(z) A temporary variable would need to be "fresh" (i.e. not clobber any existing variable). As far as I'm aware, there's no syntax for that in Python. What we can do is create a fresh scope, so that the temporary variable would merely shadow an existing binding rather than overwrite it. We can do that with a lambda and the new `:=` syntax: (lambda z: (x := z[0], y := z[1]))((y, x)) However, this alters the semantics because the stack will be different. For example, we might have a class which forbids some attribute from being altered: class A(object): def __init__(self, x): super(A, self).__setattr__('x', x) def __setattr__(self, name, value): if name == "x": raise Exception("Don't override 'x'") return super(A, self).__setattr__(name, value) This will raise an exception if we try to swap two attributes: >>> a = A('foo') >>> a.y = 'bar' >>> print(repr({'x': a.x, 'y': a.y})) {'y': 'bar', 'x': 'foo'} >>> a.x, a.y = (a.y, a.x) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 6, in __setattr__ Exception: Don't override 'x' If we replace this with the lambda version above, the exception will have a different stack trace, which we can catch and process in arbitrary ways. For example, maybe we know that the `foo` function will trigger these exceptions when given `A` objects, but it's a recoverable error. So we "ask for forgiveness instead of permission" by catching these exceptions somewhere, looking checking the stack trace to see if the Nth stack frame is `foo`, and abort if it wasn't. If we "desugared" using the above lambda, the Nth stack frame source of the exception would be a different function (`<lambda>` instead of `foo`) and hence such a program would abort. On the one hand, that's a pretty crappy program. But on the other it demonstrates that "use a temporary variable" is not "easy" in the general case (which is what language implementations must handle).
- dragonwriter 8y ago> That's why := is just syntactic sugar; there are no new semantics. There seem to be new semantics in the interaction with comprehensions, which is one of the main sources of controversy in the discussion these linked as the OP.