self.all_bullets = self.all_bullets[-10:]
prunes turtles from your local list, but not from the application's memory. There are still turtles being managed by the turtle
module even if they're not in your list. You can take a peek at turtle.turtles()
to see how many turtles are being tracked internally.
Here's a minimal reproduction:
import turtle
from random import randint
from time import sleep
turtle.tracer(0)
turtles = []
while True:
t = turtle.Turtle()
turtles.append(t)
if len(turtles) > 10:
turtles = turtles[-10:]
for t in turtles:
if randint(0, 9) < 1:
t.left(randint(0, 360))
t.forward(randint(2, 10))
print(len(turtles), len(turtle.turtles()))
turtle.update()
sleep(0.1)
You'll see the screen flood with turtles, and while your list stays at length 10, the turtle module's length keeps going up.
The first thought might be to chop off the turtle's internal list, but I prefer not to mess with library-managed resources without being given permission to do so. A quick attempt using memory stat code from this answer leaks:
import os, psutil
import turtle
from random import randint
from time import sleep
turtle.tracer(0)
while True:
t = turtle.Turtle()
if len(turtle.turtles()) > 10:
turtle.turtles()[:] = turtle.turtles()[:10]
for t in turtle.turtles():
if randint(0, 9) < 1:
t.left(randint(0, 360))
t.forward(randint(2, 10))
print(len(turtle.turtles()))
process = psutil.Process(os.getpid())
print(process.memory_info().rss) # in bytes
turtle.update()
sleep(0.1)
How to fully delete a turtle is the canonical resource for deleting turtles, but a probably better solution for this use case is to pre-allocate a turtle pool and recycle turtles from it. When a bullet (or any other entity) is created, borrow a turtle from the pool and store it as a property on your object. When the bullet leaves the screen (or meets some other termination condition), return the turtle that the bullet instance borrowed back to the pool.
Here's an example, possibly overengineered for your use case, but you can use the pool as a library or adapt the high-level concept.
import turtle
from random import randint
class TurtlePool:
def __init__(self, initial_size=8, capacity=32):
if initial_size > capacity:
raise ArgumentError("initial_size cannot be greater than capacity")
self.capacity = capacity
self.allocated = 0
self.dead = []
for _ in range(initial_size):
self._allocate()
def available(self):
return bool(self.dead) or self.allocated < self.capacity
def acquire(self):
if not self.dead:
if self.allocated < self.capacity:
self._allocate()
return self.dead.pop()
def give_back(self, t):
self.dead.append(t)
self._clean_turtle(t)
def _allocate(self):
t = turtle.Turtle()
self.allocated += 1
assert self.allocated == len(turtle.turtles())
self.dead.append(t)
self._clean_turtle(t)
def _clean_turtle(self, t):
t.reset()
t.hideturtle()
t.penup()
class Bullet:
def __init__(self, x, y, speed, angle):
self.speed = speed
self.turtle = turtle_pool.acquire()
self.turtle.goto(x, y)
self.turtle.setheading(angle)
self.turtle.showturtle()
def forward(self):
self.turtle.forward(self.speed)
def destroy(self):
turtle_pool.give_back(self.turtle)
def in_bounds(self):
x, y = self.turtle.pos()
return (
x >= w / -2 and x < w / 2 and
y >= h / -2 and y < h / 2
)
def tick():
if randint(0, 1) == 0 and turtle_pool.available():
bullets.append(Bullet(
x=0,
y=0,
speed=8,
angle=randint(0, 360)
))
next_gen = []
for b in bullets:
b.forward()
if b.in_bounds():
next_gen.append(b)
else:
b.destroy()
bullets[:] = next_gen
turtle.update()
turtle.Screen().ontimer(tick, frame_delay_ms)
frame_delay_ms = 1000 // 30
turtle.tracer(0)
turtle_pool = TurtlePool()
w = turtle.window_width()
h = turtle.window_height()
bullets = []
tick()
turtle.exitonclick()
Keep in mind, this doesn't reuse Bullet
objects and allocates a new list per frame, so there's room for further optimization.