I'm making a game using the turtle
standard library module for graphics. I have working code that creates a subclass of Turtle
, like so:
import random
class Food(Turtle):
def __init__(self):
super().__init__()
# more code...
However, I wanted to see if I could make it work without using inheritance instead. This is my attempt:
from turtle import Turtle
from random import randint
class Food():
def __init__(self):
self.food = Turtle()
# more code, but now modifying `self.food` instead of `self`
Elsewhere in the program, I have an instance food
of the Food
class, an I try to do collision detection between the food
and another Turtle, snake.head
:
if snake.head.distance(food) < 15:
...
In the original code, it works fine, but with the new version I get this error message:
Traceback (most recent call last):
File "D:\PycharmProjects\Various stuff beginning\Snake_retry\main.py", line 29, in <module>
if snake.head.distance(food) < 15:
File "C:\Users\palliativo\AppData\Local\Programs\Python\Python310\lib\turtle.py", line 1858, in distance
return abs(pos - self._position)
UnboundLocalError: local variable 'pos' referenced before assignment
Process finished with exit code 1
Why does this occur, and how can I fix it?
CodePudding user response:
In general: when using composition instead of inheritance (what the second attempt does), it is necessary to delegate everything that needs to use the internal object - including any calls to library code. Using composition instead of inheritance entails not using inheritance; therefore, the class is no longer a subtype of whatever library class; therefore, other code can't use it the same way.
When the .distance
method is called on the snake.head
, the Turtle class needs to be given something whose position it knows how to find. It has the following rules (quoting from the source):
if y is not None:
pos = Vec2D(x, y)
if isinstance(x, Vec2D):
pos = x
elif isinstance(x, tuple):
pos = Vec2D(*x)
elif isinstance(x, TNavigator):
pos = x._position
(This is slightly offset from the error message reported, because of changes between Python versions.)
In other words, it knows how to use:
- two separate integers
x
andy
(i.e., directly telling it the position); - a
Vec2D
(directly telling it the position, but using the class that the library provides for representing positions); - a tuple (directly telling it the position, but using values stored in a tuple);
- a
TNavigator
(in practice, this means a Turtle, but there are other possibilities).
When Food
inherits from Turtle
, it's a Turtle
. It has built-in position tracking, which other Turtle
s can use: accessing the hidden ._position
attribute. (The leading underscore means that other classes aren't supposed to know about it or use it; but Python does not have true privacy.)
When Food
stores a Turtle
, it isn't a Turtle
. While the logic is obvious to the programmer - fetch the turtle stored in the .food
attribute, and then get the position of that - the already-written Turtle
code has no way to know that.
To fix the problem, we can extract the underlying Turtle
at the point where the method is called:
if snake.head.distance(food.food) < 15:
...
Or we can implement the interface that the library code wants to use. In this specific instance, that won't be feasible; it's explicitly checking for types, so we'd need the class to be one of those types - in which case, we might as well just use inheritance in the first place.
But consider another example, where someone else has written a distance function (not method) that expects two Turtles:
def public_distance(t1, t2):
# this version uses the interface that the Turtle class provides
# for other code to get the position: calling the `pos` method
return abs(t1.pos() - t2.pos())
def private_distance(t1, t2):
# this version directly (naughtily) accesses the "private" attribute
return abs(t1._position - t2._position)
Then we could adapt the class to meet that interface. For a missing method, implement the method, and have its logic check the wrapped object. For a missing attribute, use a (read-only) property that checks for the corresponding information in the wrapped object. Here's an example showing both (and using the property to implement the method):
class Food:
# other stuff as before...
@property
def _position(self):
return self.food._position
def pos(self):
return self._position
(One might ask, why does the Turtle class use a method pos
, instead of a property
? That's because it's old code following an old design, before property
support was added to Python. Updating things like this is low priority for the Python dev team; it risks breaking old code; and it involves writing new documentation - and hoping that tutorial authors get the hint as well.)
CodePudding user response:
I think you'll have to do
if snake.head.distance(food.food) < 15:
food.generate_food()