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