Why does a library class break when I try to store and use an instance (delegation) instead of making a subclass (inheritance)?

Question:

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:PycharmProjectsVarious stuff beginningSnake_retrymain.py", line 29, in <module>
    if snake.head.distance(food) < 15:
  File "C:UserspalliativoAppDataLocalProgramsPythonPython310libturtle.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?

Asked By: palliativo

||

Answers:

I think you’ll have to do

if snake.head.distance(food.food) < 15:
    food.generate_food()
Answered By: WingedSeal

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 and y (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 Turtles 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.)

Answered By: Karl Knechtel