0

Having read...

How can I log key presses using turtle?

I am trying to detect key presses using a slightly different method.

Here is a simplified version of my code, which works as expected...

from turtle import *

WIDTH, HEIGHT = 500, 500
screen = Screen()
screen.setup(WIDTH, HEIGHT)
bgcolor('grey')
ht()
pu()
    
def checka():
    write('a')
    fd(10)
def checkb():
    write('b')
    fd(10)

screen.onkey(checka, 'a')
screen.onkey(checkb, 'b')

screen.listen()
screen.mainloop()

However I wish to handle key presses for all letters of the alphabet, so tried this...

from turtle import *

WIDTH, HEIGHT = 500, 500
screen = Screen()
screen.setup(WIDTH, HEIGHT)
bgcolor('grey')
ht()
pu()
    
def check(l):
    write(l)
    fd(10)

screen.onkey(check('a'), 'a')
screen.onkey(check('b'), 'b')

screen.listen()
screen.mainloop()

But this code does not work. Can anyone shed any light on what is happening here or suggest an alternative (but as simple) method of achieving the same?

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Rich
  • 27
  • 5
  • @quamrana Thank you! Perfect solution - I need to study lambda functions a bit more – Rich Jun 04 '21 at 11:12

3 Answers3

2

I'm guessing that screen.onkey() takes a function which it calls.

Your code: screen.onkey(check('a'), 'a') instead calls the function and returns None which is not a function.

You can create your own function using a lambda like this:

screen.onkey(lambda :check('a'), 'a')

And if you want to call onkey() for each letter of the alphabet, then you can easily have a loop, but without falling into the scope problem:

import string

for c in string.ascii_lowercase:
    screen.onkey(lambda c=c:check(c), c)
quamrana
  • 37,849
  • 12
  • 53
  • 71
1

The screen.onkey() function expects a function as input. In your first example you do this correctly (screen.onkey(checka, 'a')), but in the second example, you call the function before you pass it (screen.onkey(check('a'), 'a'). This means that you are passing the return value of the check function, not the function itself.

Your check function doesn't have any return statements in that return a value explicitly. In Python, functions that don't return a value explicitly return None. So you are in effect calling screen.onkey(None, 'a'), which I am guessing does not have any effect.

To fix this, you can use a closure - a function inside a function. With closures, the inner function can use variables available to the outer function, which means you can make check functions for any letter.

def make_check_func(l):
    def check():
        write(l)
        fd(10)
    
    return check

screen.onkey(make_check_func('a'), 'a')
screen.onkey(make_check_func('b'), 'b')

Or, as quamrana suggests, you can do the same thing with less code by using lambda functions.

def check(l):
    write(l)
    fd(10)

screen.onkey(lambda: check('a'), 'a')
screen.onkey(lambda: check('b'), 'b')

--EDIT--

To add functions for all of the letters in the alphabet, you can use a for loop. Conveniently, Python has already defined a string containing all the lowercase ASCII characters at string.ascii_lowercase, so we can use that to loop over.

import string

def make_check_func(l):
    def check():
        write(l)
        fd(10)
    
    return check

for l in string.ascii_lowercase:
    screen.onkey(make_check_func(l), l)

Here, the body of the for loop will be run once for each character in the string, and the value of l will be that character. This has the effect of running screen.onkey(make_check_func('a'), 'a'), then screen.onkey(make_check_func('b'), 'b'), all the way through to screen.onkey(make_check_func('z'), 'z').

Note that using screen.onkey(lambda: check(l), l) inside the for loop will not work, as the value of l that the lambda function "remembers" will be always be "z". See the common gotchas entry for an explanation.

Jack Taylor
  • 5,588
  • 19
  • 35
  • that is an excellent and very clear description of what is happening. I find the make_check_function rather amusing as I assume that it calls the make_check_function to carry out the actual operation and passing in the data but then after completing and returning the function name 'check' then the var 'l' will have no value so will write nothing. I can see how the function 'lambda' will do the same thing and this has helped my understanding of those. If I think about it too much my head starts to hurt! Thank you! – Rich Jun 04 '21 at 12:36
  • So by following the above advice I now have code working as I wished, but to re-iterate an original point, is there a more efficient way to achieve the same as the 26 lines of code for each letter of the alphabet that I now have, viz: screen.onkey(lambda:check('a'), 'a') screen.onkey(lambda:check('b'), 'b') screen.onkey(lambda:check('c'), 'c') screen.onkey(lambda:check('d'), 'd') – Rich Jun 04 '21 at 12:43
  • I've updated my answer with an example of how to register all the functions in two lines of code. – Jack Taylor Jun 05 '21 at 14:41
  • > I assume that it calls the make_check_function to carry out the actual operation This is not quite right. The actual operation is done by the `check` function, and `return check` returns a function object, not the function name. The `check` function "remembers" the value of `l`, even after `make_check_function` has exited. To do this, each time you run `make_check_function`, it makes a different `check` function, with a reference to a different `l` value. In memory, there will be several `check` functions, not just one. – Jack Taylor Jun 05 '21 at 14:48
  • This is pretty tough to explain in a comment, so I recommend reading the Real Python article: https://realpython.com/inner-functions-what-are-they-good-for/ – Jack Taylor Jun 05 '21 at 14:52
  • THANK YOU - that is all incredibly useful. I will now go and read up on inner functions! – Rich Jun 07 '21 at 11:33
  • @JackTaylor: I'm sure the loop in your last code snippet doesn't do what you think it does. – quamrana Jun 07 '21 at 13:13
  • @quamrana Shoot, you are right. I've edited my answer. I should know better, as I've been bitten by this before. Rich: make sure you use my updated answer! – Jack Taylor Jun 07 '21 at 21:29
1

Although this can be solved using a lambda as @quamrana demonstrates, or using a closure as @JackTaylor explains in detail, I'm partial to partial for this sort of problem:

from turtle import Screen, Turtle
from string import ascii_letters
from functools import partial

WIDTH, HEIGHT = 500, 500

def check(letter):
    turtle.write(letter)
    turtle.forward(10)

screen = Screen()
screen.setup(WIDTH, HEIGHT)
screen.bgcolor('grey')

turtle = Turtle()
turtle.hideturtle()
turtle.penup()

for letter in ascii_letters:
    screen.onkey(partial(check, letter), letter)

screen.listen()
screen.mainloop()

The partial function creates a new function where some arguments of the orginal function have been "locked in".

cdlane
  • 40,441
  • 5
  • 32
  • 81