Robin Andrews
Robin Andrews

Reputation: 3794

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()

Upvotes: 0

Views: 499

Answers (2)

cdlane
cdlane

Reputation: 41872

If I wanted to fix this but retain the flavor of your original approach, to tidy it up, I'd use a partial() from functools instead of a lambda (along with an update_wrapper() to make it palatable to ontimer()). I'd also remove the assumption that the path starts at (0, 0) and start with the pen up, dropping it after moving to the first coordinate:

from turtle import Screen, Turtle
from functools import partial, update_wrapper

path = [ \
    (100, 180), (90, 170), (95, 150), (80, 135), (65, 145), \
    (55, 140), (70, 125), (65, 105), (75, 85), (60, 70), \
    (70, 60), (80, 75), (100, 65), (120, 75), (130, 60), \
    (140, 70), (125, 85), (135, 105), (130, 125), (145, 140), \
    (135, 145), (120, 135), (105, 150), (110, 170), (100, 180), \
]

def do_path(a_list):
    position, *rest = a_list

    bob.setposition(position)
    bob.pendown()

    if rest:
        wrapper = partial(do_path, rest)
        update_wrapper(wrapper, do_path)
        screen.ontimer(wrapper, 500)
    else:
        bob.hideturtle()

screen = Screen()

bob = Turtle('square')
bob.penup()

do_path(path)

screen.exitonclick()

Upvotes: 1

Paul M.
Paul M.

Reputation: 10799

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.

Upvotes: 1

Related Questions