If you looked at the variables, repetition and commands chapters, you already know a great deal about how to keep your code tidy and organized. Variables can be used to store data, commands to manipulate data, and for-loops to do things repeatedly. As you move on to bigger projects you’ll probably want to learn about classes as well. A class is a programming convention that defines a thing (or object) together with all of the thing’s properties (e.g. variables) and behavior methods (e.g. commands). If you take a look at the source code of PlotDevice libraries you’ll notice that they are full of classes.

For example, a Creature class would consist of all the traits shared by different creatures, such as size, position, speed and direction (properties), and avoid_obstacle(), eat_food() (methods).

We can then use the Creature class to generate many different instances of a creature. For example, an ant is a small and speedy creature that wanders around randomly looking for food, avoiding obstacles by going around them. A dinosaur is also a creature, but bigger and slower. It eats anything crossing its path and avoids obstacles by crashing through them.

Classes in Python

As we saw in the Commands chapter, there’s a difference between defining something and using it. This same distinction also applies to classes: you first need to teach PlotDevice about your new object type, then later in the script you can create as many ‘instances’ of the type as you want.

Class definitions have a syntax that looks quite similar to what we’ve seen before. There’s a line starting with the class statement where we set the name for our new class, then an indented block of one or more method definitions. As always, don’t forget the colon that begins the indented block:

class ClassName:
    ... # method defintions

Class definition and initialization

Every class has a method named __init__() which is executed once when an instance of the class is created. This method sets all the starting values for an object’s properties and defines the parameters accepted when creating new objects.

For example, here’s what a simple definition of a Creature class would look like:

class Creature:
    def __init__(self, x, y, speed=1.0, size=4):
        self.x = x
        self.y = y
        self.speed = speed
        self.size = size

As you can see, a creature has x and y properties (its location in space) as well as speed and size properties (which we’ll use to move the creature around). Since speed and size are defined with default values, they are optional when instantiating a new creature.

You create instances of a class by calling the class name as if it were a command, but including the parameters defined in its __init__() method. The one tricky bit is that you should ignore the self argument, since it will be filled in automatically (see below).

ant = Creature(100, 100, speed=2.0)
dinosaur = Creature(200, 250, speed=0.25, size=45)

To change a property value for a creature later on we can simply re-assign it:

ant.speed = 2.5

As with command names, Python has some conventions for naming classes that you should try to follow:

Methods and the self parameter

Let’s add a roam() method to the Creature class to move creatures around randomly. A class method looks exactly like a command definition but it always takes self as the first parameter. The self parameter is the current instance of the class. It allows you to access all of an object’s current property values and methods from inside the method.

class Creature:
    def __init__(self, x, y, speed=1.0, size=4):
        self.x = x
        self.y = y
        self.speed = speed
        self.size = size

        self._vx = 0
        self._vy = 0

    def roam(self):
        """ Creature changes heading aimlessly.
        """

        v = self.speed
        self._vx += random(-v, v)
        self._vy += random(-v, v)
        self._vx = max(-v, min(self._vx, v))
        self._vy = max(-v, min(self._vy, v))

        self.x += self._vx
        self.y += self._vy

So now we have added two new properties to the class, _vx and _vy, which store a creature’s current heading. Both property names start with an underscore because they are for private use inside the class methods (i.e., no one should directly manipulate ant._dx from the outside).

Each time the roam() method is called we add or subtract a random proportion of the creature’s speed from its heading, like a pinwheel twirling around randomly. We also make sure the horizontal and vertical ‘velocities’ don’t exceed the creature’s maximum speed. Otherwise the creature would start going faster and faster which isn’t very realistic. Finally, we update the creature’s position by stepping it toward the new heading.

Now we can create two ants (a small fast one and a big slow one). Since their size and speed differ they will roam in different ways.

ant1 = Creature(300, 300, speed=2.0)
ant2 = Creature(300, 300, speed=0.5, size=8)

speed(30)
def draw():
    ant1.roam()
    ant2.roam()
    arc(ant1.x, ant1.y, radius=ant1.size)
    arc(ant2.x, ant2.y, radius=ant2.size)
classes-creature1

Play movie

Case Study: ‘Intelligent’ Agents

Now that we know the basics about classes, properties and methods, we can take things a lot further. We can define different classes for different things and have them interact with each other.

If we want to have our little critters avoid obstacles while wandering around, they will need to have some sense of the world around them. Our code will need to keep track of where the obstacles are located, and the creature will need to have some sort of ‘feeler’ so it can detect looming collisions.

A feeler could be the creature’s sight, hearing, or antennae – it’s really just a metaphor for the sensory feedback we’ll be providing it from ‘the world’. For our simple class it can represented as an x/y ‘test’ point just in front of the creature’s current heading. If this point falls inside an obstacle, it’s time for the creature to change its direction.

classes-creature2

The creature is heading into an obstacle – it can ‘sense’ the obstacle with its feeler and will need to adjust its bearing clockwise to avoid colliding with it.

World class

We’ll start out by creating a World class which we can use to store a list of obstacles. Later on we can add all sorts of other stuff to the world (e.g. weather methods, colony location properties, etc.). Note that we’re defining it as a class even though we’ll only ever create one World object in any of our simulations.

class World:
    def __init__(self):
        self.obstacles = []
 

Obstacle class

Next we’ll define an Obstacle class. In our simulation we’ll be creating a number of Obstacle objects and storing them in a shared World object. The Obstacle itself doesn’t need to know about the world-at-large (since all it ‘does’ is occupy space), so it only keeps track of its personal location and size:

class Obstacle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
 

Geometry

To know if a creature is going to run into an obstacle we need to know if the tip of its feeler intersects with an obstacle. Obviously we’re going to need some math to calculate the coordinates of the tip of the feeler. Luckily everything we need is already described in the tutorial on geometry. We could use the methods provided by Point objects, but instead let’s define some equivalent geometry commands that work with plain-old numbers:

from math import degrees, atan2
from math import sqrt, pow
from math import radians, sin, cos

def angle(x0, y0, x1, y1):
    """Returns the angle between two points."""
    return degrees( atan2(y1-y0, x1-x0) )

def distance(x0, y0, x1, y1):
    """Returns the distance between two points."""
    return sqrt(pow(x1-x0, 2) + pow(y1-y0, 2))

def coordinates(x0, y0, distance, angle):
    """Returns the coordinates of given distance and angle from a point."""
    return (x0 + cos(radians(angle)) * distance,
            y0 + sin(radians(angle)) * distance)

def reflect(x0, y0, x1, y1, d=1.0, a=180):
    """Returns the coordinates of x1/y1 reflected through x0/y0"""
    d *= distance(x0, y0, x1, y1)
    a += angle(x0, y0, x1, y1)
    x, y = coordinates(x0, y0, d, a)
    return x, y
 

With the above coordinates() command we can:


Obstacle avoidance

So first of all we’ll need to add a new feeler_length property to the Creature class, and a new heading() method that calculates the angle between a creature’s current position and its next position. We also add a new world property to the Creature class. That way each creature has access to all the obstacles in the world. We loop through the list of the world’s obstacles in the avoid_obstacles() method.

The avoid_obstacles() method will do the following:

class Creature:

    def __init__(self, world, x, y, speed=1.0, size=4):
        self.x = x
        self.y = y
        self.speed = speed
        self.size = size

        self.world = world
        self.feeler_length = 25

        self._vx = 0
        self._vy = 0

    def heading(self):
        """ Returns the creature's heading as angle in degrees.
        """

        return angle(self.x, self.y, self.x+self._vx, self.y+self._vy)

    def avoid_obstacles(self, m=0.4, perimeter=4):
        # Find out where the creature is going.
        a = self.heading()

        for obstacle in self.world.obstacles:

            # Calculate the distance between the creature and the obstacle.
            d = distance(self.x, self.y, obstacle.x, obstacle.y)

            # Respond faster if the creature is very close to an obstacle.
            if d - obstacle.radius < perimeter: m *= 10

            # Check if the tip of the feeler falls inside the obstacle.
            # This is never true if the feeler length
            # is smaller than the distance to the obstable.
            if d - obstacle.radius <= self.feeler_length:
                tip_x, tip_y = coordinates(self.x, self.y, d, a)
                if distance(obstacle.x, obstacle.y,
                            tip_x, tip_y) < obstacle.radius:

                    # Nudge the creature away from the obstacle.
                    m *= self.speed
                    if tip_x < obstacle.x: self._vx -= random(m)
                    if tip_y < obstacle.y: self._vy -= random(m)
                    if tip_x > obstacle.x: self._vx += random(m)
                    if tip_y > obstacle.y: self._vy += random(m)

                    if d - obstacle.radius < perimeter: return

    def roam(self):
        """ Creature changes heading aimlessly.
        With its feeler it will scan for obstacles and steer away.
        """

        self.avoid_obstacles()

        v = self.speed
        self._vx += random(-v, v)
        self._vy += random(-v, v)
        self._vx = max(-v, min(self._vx, v))
        self._vy = max(-v, min(self._vy, v))

        self.x += self._vx
        self.y += self._vy
classes-creature3

Play movie | view source code

Another easy example of classes is the Tendrils item in the gallery.