Search code examples
pythonturtle-graphicspython-turtlepartial-application

Why does this method of detecting keypresses with the Turtle module in Python not work?


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?


Solution

  • 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.