Search code examples
pythontimerturtle-graphics

Python Turtle Move Through List of Coordinates on Timer


I'm trying to get Python Turtle to move through a list of coordinates using a timer.There are probably many ways to do this, but with my current attempt, the program just hangs. Could someone explain why please?

import turtle

path = [(0, 0), (10, 10), (10, 20), (30, 40)]
bob = turtle.Turtle("square")

def do_path(a_list):
    x, y = a_list[0]
    bob.goto(x, y)
    while len(a_list) > 0:
        turtle.ontimer(lambda: do_path(a_list[1:]), 500)

do_path(path)
turtle.done()

Using a global variable doesn't seem to help either:

import turtle

path = [(0, 0), (10, 10), (10, 20), (30, 40)]
bob = turtle.Turtle("square")

def do_path():
    global path
    x, y = path.pop(0)
    bob.goto(x, y)
    while len(path) > 0:
        turtle.ontimer(lambda: do_path(path), 500)

do_path()
turtle.done()

Solution

  • That recursive call in a while loop looks scary to me - the while loop will never end for all depths of the recursion where len(a_list) != 0. More like this maybe?

    import turtle
    
    coordinates = [
        (0, 0),
        (10, 10),
        (10, 20),
        (30, 40)
    ]
    
    coordinates_iter = iter(coordinates)
    
    t = turtle.Turtle("square")
    
    def go_to_next_coord():
        try:
            next_coord = next(coordinates_iter)
        except StopIteration:
            return
        t.goto(next_coord)
        turtle.ontimer(go_to_next_coord, 500)
    
    go_to_next_coord()
    turtle.done()
    

    So lambda: do_path(a_list[1:]) doesn't modify a_list? In a recursive function call it would no?

    Definitely not! You're just slicing a_list and passing that (completely independent) list to do_path as an argument. a_list from the very first recursion will not have changed in size, so the while loop happly hangs as your do_path waits to finish.

    EDIT - on the topic of whether or not it's really "recursion":

    import turtle
    
    def foo(depth):
        print(f"Starting depth {depth}")
        if depth != 5:
            turtle.ontimer(lambda: foo(depth+1), 1000)
        print(f"Ending depth {depth}")
    
    foo(0)
    

    Output:

    Starting depth 0
    Ending depth 0
    Starting depth 1
    Ending depth 1
    Starting depth 2
    Ending depth 2
    Starting depth 3
    Ending depth 3
    Starting depth 4
    Ending depth 4
    Starting depth 5
    Ending depth 5
    

    Looks like it's technically not strictly recursive at all! It seems turtle has a way of scheduling these callbacks. The output you can expect to see in a recursive setup would have looked like this:

    Starting depth 0
    Starting depth 1
    Starting depth 2
    Starting depth 3
    Starting depth 4
    Starting depth 5
    Ending depth 5
    Ending depth 4
    Ending depth 3
    Ending depth 2
    Ending depth 1
    Ending depth 0
    

    However, the issue you're having doesn't have anything to do with recursion or turtle in general. To be precise, it has to do with a misunderstanding of the call-stack and/or potentially list slicing. Take a look at this example code:

    def do_it(depth, items):
        length = len(items)
        print(f"I'm recursion depth {depth}, I see {length} item(s).")
        if depth != 5: #arbitrary base case:
            new_items = items[1:]
            print(f"Depth {depth} - items: {items}")
            print(f"Depth {depth} - new_items: {new_items}")
            do_it(depth+1, new_items)
        print(f"Depth {depth} is ending now, length is {length} and items is {items}")
    
    do_it(0, [1, 2, 3, 4, 5])
    

    Output:

    I'm recursion depth 0, I see 5 item(s).
    Depth 0 - items: [1, 2, 3, 4, 5]
    Depth 0 - new_items: [2, 3, 4, 5]
    I'm recursion depth 1, I see 4 item(s).
    Depth 1 - items: [2, 3, 4, 5]
    Depth 1 - new_items: [3, 4, 5]
    I'm recursion depth 2, I see 3 item(s).
    Depth 2 - items: [3, 4, 5]
    Depth 2 - new_items: [4, 5]
    I'm recursion depth 3, I see 2 item(s).
    Depth 3 - items: [4, 5]
    Depth 3 - new_items: [5]
    I'm recursion depth 4, I see 1 item(s).
    Depth 4 - items: [5]
    Depth 4 - new_items: []
    I'm recursion depth 5, I see 0 item(s).
    Depth 5 is ending now, length is 0 and items is []
    Depth 4 is ending now, length is 1 and items is [5]
    Depth 3 is ending now, length is 2 and items is [4, 5]
    Depth 2 is ending now, length is 3 and items is [3, 4, 5]
    Depth 1 is ending now, length is 4 and items is [2, 3, 4, 5]
    Depth 0 is ending now, length is 5 and items is [1, 2, 3, 4, 5]
    >>> 
    

    I know the output is a bit dense to follow, but hopefully it should demonstrate a misconception you seem to be having. Just because you call a new function (or the same function in the case of recursion) within a function, doesn't mean that the function you're "leaving" ends or terminates. The function you left is waiting on the call-stack until the function you went to terminates, and then execution comes back to the calling-function. All I'm really trying to highlight here is that the different "depths" (functions sitting on the call-stack) see different things. The example I used here is recursive, but the same thing applies in your non-recursive case. Just because you called do_path inside of do_path doesn't mean that the old do_path just suddenly goes away. It's waiting for the inner, most recent invokation of do_path to finish until it can finish.