1

I just put my hands on SHA2017 badge. I wrote the following program and ran it in a simulator:

import ugfx
import badge

def key_pressed(key, is_pressed):
    print('key_pressed(key=%r, is_pressed=%r)' % (key, is_pressed))

badge.init()
ugfx.init()
ugfx.input_init()
for key in [ugfx.JOY_LEFT, ugfx.JOY_UP, ugfx.JOY_RIGHT, ugfx.JOY_DOWN]:
    ugfx.input_attach(key, (lambda is_pressed: key_pressed(key, is_pressed)))
# Uncommenting this makes all keystrokes get interpreted as JOY_UP:
# ugfx.input_attach(key, (lambda is_pressed: key_pressed(key, is_pressed)))
# But uncommenting this makes JOY_UP work as intended and doesn't affect other keys
# ugfx.input_attach(ugfx.JOY_UP, (lambda is_pressed: key_pressed(ugfx.JOY_UP, is_pressed)))
while True:
    pass

If I run this, pressing any key will say that I pressed JOY_DOWN. Uncommenting the first line will make it all say I pressed JOY_UP and only uncommenting the last commented line will make all keys fire JOY_DOWN, apart from JOY_UP, which works as intended.

Is this a bug or intended behavior? It seems like MicroPython is compiling the lambda only once and ties a reference to "key", which seems odd to me.

dda
  • 6,030
  • 2
  • 25
  • 34
d33tah
  • 10,999
  • 13
  • 68
  • 158
  • The commented lines appear to be identical. – user2357112 Aug 08 '17 at 17:31
  • @user2357112 you're right, I just changed that – d33tah Aug 08 '17 at 17:33
  • Unlike in many other languages, `lambda` in Python does not form a closure; it still refers to the outside variables, not local copies thereof. – 9000 Aug 08 '17 at 17:58
  • @9000: "Closure" doesn't imply copying the closure variables. Closures traditionally *don't* make copies. – user2357112 Aug 08 '17 at 19:40
  • @user2357112: [Wikipedia](https://en.wikipedia.org/wiki/Closure_(computer_programming)#Implementation_and_theory) refers to what I tried to mean: "When the closure is entered at a later time, possibly with a different lexical environment, the function is executed with its non-local variables referring to the ones captured by the closure, _not the current environment_." (emph. mine). This, of course, depends on what we consider the local environment; the lambda does not leave it (e.g. by being passed to somewhere else). – 9000 Aug 08 '17 at 19:51
  • @9000: That's not saying to use the *state* of the environment at the time the closure was defined. It's saying that if I do `def f(x): return lambda: x`, `closure = f(3)`, `def g(x): return closure()`, and `g(4)`, the `closure()` call uses the `x` from the environment of the `f` call, not the `g` call. The environment can still mutate after the closure's creation, and the closure will reflect such mutations. – user2357112 Aug 08 '17 at 19:58
  • It's about lexical vs dynamic scope, not about making copies to protect against mutations. – user2357112 Aug 08 '17 at 20:04
  • @user2357112: Thanks, I stand corrected. – 9000 Aug 08 '17 at 20:12

1 Answers1

2

The problem is that the variable key is updated, and thus all references in the lambda expressions are updated as well:

You need to scope key as well:

for key in [ugfx.JOY_LEFT, ugfx.JOY_UP, ugfx.JOY_RIGHT, ugfx.JOY_DOWN]:
    ugfx.input_attach(key,lambda k: (lambda is_pressed: key_pressed(k, is_pressed)(key)))

Or you can use:

from functools import partial

for key in [ugfx.JOY_LEFT, ugfx.JOY_UP, ugfx.JOY_RIGHT, ugfx.JOY_DOWN]:
    ugfx.input_attach(key,partial(lambda k, is_pressed: key_pressed(k, is_pressed),key))

Or work with default values:

from functools import partial

for key in [ugfx.JOY_LEFT, ugfx.JOY_UP, ugfx.JOY_RIGHT, ugfx.JOY_DOWN]:
    ugfx.input_attach(key,lambda is_pressed, k=key: key_pressed(k, is_pressed)))
d33tah
  • 10,999
  • 13
  • 68
  • 158
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555