Third Clone Of The Turtle

Question:

I made this program, when trying to make a chase game, but I stumbled along something really strange. I created a clone of the turtle, but at the middle of the map a third one appeared.

Does anybody know what causes this?

import turtle

sc = turtle.Screen()
t = turtle.Turtle()
c = turtle.clone()
c.penup
t.penup
c.goto(100,100)

def turnleft():
    t.left(30)

def turnright():
    t.right(30)

while True:
    t.forward(2)
    c.forward(2)
    sc.onkey(turnleft, "Left")
    sc.onkey(turnright, "Right")
    sc.listen()
Asked By: Klap

||

Answers:

Very good question. I’m able to reproduce the behavior: if you only make one turtle, print(len(turtle.turtles())) gives 1 as expected, but after cloning once, there’s suddenly 3. Here’s a minimal example:

import turtle

t = turtle.Turtle()
print(len(turtle.turtles())) # => 1, no problem
c = turtle.clone()
print(len(turtle.turtles())) # => 3 !!?

The problem is calling .clone() on turtle (the module) rather than on the turtle instance you want to clone:

import turtle

t = turtle.Turtle()
c = t.clone()
print(len(turtle.turtles())) # => 2 as expected

This is a classic turtle gotcha: mistaking the functional interface for the object-oriented interface. When you call turtle.clone(), it’s a functional call, so the module creates its singleton non-OOP turtle, then clones it and returns the clone which you store in c.

The inimitable cdlane advocates for the following import:

from turtle import Screen, Turtle

which makes it hard to mess things up.


If you’re curious about the CPython turtle internals that cause the behavior to occur, here’s the code (from Lib/turtle.py#L3956):

## The following mechanism makes all methods of RawTurtle and Turtle available
## as functions. So we can enhance, change, add, delete methods to these
## classes and do not need to change anything here.

__func_body = """
def {name}{paramslist}:
    if {obj} is None:
        if not TurtleScreen._RUNNING:
            TurtleScreen._RUNNING = True
            raise Terminator
        {obj} = {init}
    try:
        return {obj}.{name}{argslist}
    except TK.TclError:
        if not TurtleScreen._RUNNING:
            TurtleScreen._RUNNING = True
            raise Terminator
        raise
"""

def _make_global_funcs(functions, cls, obj, init, docrevise):
    for methodname in functions:
        method = getattr(cls, methodname)
        pl1, pl2 = getmethparlist(method)
        if pl1 == "":
            print(">>>>>>", pl1, pl2)
            continue
        defstr = __func_body.format(obj=obj, init=init, name=methodname,
                                    paramslist=pl1, argslist=pl2)
        exec(defstr, globals())
        globals()[methodname].__doc__ = docrevise(method.__doc__)

_make_global_funcs(_tg_screen_functions, _Screen,
                   'Turtle._screen', 'Screen()', _screen_docrevise)
_make_global_funcs(_tg_turtle_functions, Turtle,
                   'Turtle._pen', 'Turtle()', _turtle_docrevise)

This takes all of the turtle Screen() and Turtle() methods and wires them into the module’s globals, so turtle.Turtle().clone is set to turtle.clone. As part of this rewiring, __func_body adds a bit of boilerplate to every call which checks to see whether Turtle._pen or Turtle._screen exist already, and creates them if they don’t.


Unrelated, but you need to call pendown() with parentheses if you want it to do anything. Also, it’s a good idea to set event listeners before the loop rather than inside of it, or just remove that entirely since it’s not relevant to the question.

Answered By: ggorlen