Comp 112
Lecture 11
Classes and Interfaces2018.04.10
Suppose that we want turtle
to draw a dot at a given point.
We can give the dot an initial location:
And write a function to draw the dot at the given location:
An event handler is a function that we pass to a higher-order function in turtle.
Turtle will call the event handler whenever the specified event occurs.
Let’s try to add an event handler to move the dot.
def move_right () : # a function to move the dot to the right
dot_x += 10
def right_event () : # an event handler function
move_right ()
turtle.clear () # erase the old scene
draw_dot ()
turtle.update () # paint the new scene
def run_dot () :
turtle.tracer (0 , 0) # don't render to screen unless instructed
turtle.hideturtle () # don't show the turtle icon
turtle.penup () # default turtle state is not drawing
turtle.onkeypress (right_event , 'Right') # register an event handler
turtle.listen () # activate event handlers
This won’t work because dot_x
is a global variable, so we can’t reassign it from within a function (without some trickery).
It also won’t work to make dot_x
a local variable of the move_right
function, because then draw_dot
won’t know about it.
But if dot_x
were a value of mutable type then the move_right
could change its value and draw_dot
would be able to see it.
One solution is to introduce a new type, known as a class.
We can make a new value, called an object, of this type like this:
Each object has its own private variable table where we can store its state:
These private variables are called attributes or fields of the object.
Like dictionaries, objects of a class type are mutable: their state can change over time:
Point
s:Instead of manipulating the state of a Point
directly, we can write an interface for it.
This is a collection of functions that lets us interact with its state in a controlled way.
def set_x (point , x_coord) :
# signature: Point , int -> NoneType
point.x = x_coord
def set_y (point , y_coord) :
# signature: Point , int -> NoneType
point.y = y_coord
def get_x (point) :
# signature: Point -> int
return point.x
def get_y (point) :
# signature: Point -> int
return point.y
def move_right (point) :
# signature: Point -> NoneType
point.x += 10
Interfaces are useful so that the user doesn’t need to care about how an object’s state is represented internally.
They can also be used to prevent users from setting an object’s state to an invalid value.
Now we can use the mutable Point
object, my_point
, to fix our event handler for moving the dot:
def draw_dot (point) :
# signature: Point -> NoneType
dot_at (get_x (point) , get_y (point))
def right_event () :
move_right (my_point)
turtle.clear () # erase the old scene
draw_dot (my_point)
turtle.update () # paint the new scene
def run_dot () :
turtle.tracer (0 , 0) # don't render to screen unless instructed
turtle.hideturtle () # don't show the turtle icon
turtle.penup () # default turtle state is not drawing
turtle.onkeypress (right_event , 'Right') # register an event handler
turtle.listen () # activate event handlers
Recall that we can use the convenient method syntax for functions that act on certain types.
Unfortunately, this will not work for the Point
type yet.
We can fix this by making the interface functions part of the class:
class Point :
# note that these function definitions are all inside the class:
def set_x (point , x_coord) :
point.x = x_coord
def set_y (point , y_coord) :
point.y = y_coord
def get_x (point) :
return point.x
def get_y (point) :
return point.y
def move_right (point) :
point.x += 10
Now we can call them in either function or method form:
my_point = Point ()
Point.set_x (my_point , 0) # ordinary function syntax
my_point.set_y (0) # or using method syntax
my_point.move_right ()
It’s annoying that before we can use a Point
object, we have to manually initialize it by calling set_x
and set_y
.
We can make this initialization step automatic by defining a constructor method.
A constructor is a method with the special name “__init__
” (underscore, underscore, “init”, underscore, underscore).
Its first argument represents the newly-created object of its class; it can take any number of additional arguments.
The constructor automatically gets called whenever you create a new object of the class.
class Point :
def __init__ (self , init_x , init_y) :
# signature: Point , int , int -> NoneType
self.x = init_x
self.y = init_y
def get_x (self) :
return self.x
def get_y (self) :
return self.y
def move_right (self) :
self.x += 10
It’s conventional (but totally optional) to name the principal argument of a class method “self
” or “this
”.
We can streamline our dot-moving program now:
import turtle
dot_size = 10
def dot_at (x , y) :
turtle.goto (x , y)
turtle.dot (dot_size)
# use the constructor to create a new Point object:
my_point = Point (0 , 0)
def draw_dot (point) :
# use interface methods to access the point's state:
dot_at (point.get_x () , point.get_y ())
def right_event () :
my_point.move_right ()
turtle.clear () # erase the old scene
draw_dot (my_point)
turtle.update () # paint the new scene
def run_dot () :
turtle.tracer (0 , 0) # don't render to screen unless instructed
turtle.hideturtle () # don't show the turtle icon
turtle.penup () # default turtle state is not drawing
turtle.onkeypress (right_event , 'Right') # register an event handler
turtle.listen () # activate event handlers
We’ve seen that we can make an object look like it is moving by erasing it and then drawing it again in a slightly different location.
This is the same principle behind a “motion picture” in the cinema or on television.
For an object that “moves” under the user’s control (like the dot) all we need is an interface to get and change its location.
But for an object that “moves” on its own accord, it’s better to encode its velocity as part of its state, and add an interface method to update the state based on how much time has passed. This enables much smoother animation.
class Ball :
def __init__ (this , init_pos , init_vel) :
# signature: Ball , tuple (int , int) , tuple (int , int) -> NoneType
this.position = init_pos
this.velocity = init_vel
def get_pos (this) :
return this.position
def get_vel (this) :
return this.velocity
def update_pos (this , time) :
(p_x , p_y) = this.position # note the
(v_x , v_y) = this.velocity # tuple assignment
this.position = (p_x + (v_x * time) , p_y + (v_y * time))
import time, random, turtle
dot_size = 10
max_speed = 400
timestep = 1/60
balls = []
def draw_ball (ball) :
(x , y) = ball.get_pos ()
turtle.goto (x , y)
turtle.dot (dot_size)
def draw_scene () :
turtle.clear () # erase the old scene
for ball in balls :
draw_ball (ball)
turtle.update () # paint the new scene
def add_ball () :
x_vel = random.randint (-max_speed , max_speed)
y_vel = random.randint (-max_speed , max_speed)
new_ball = Ball ((0 , 0) , (x_vel , y_vel)) # calls the Ball constructor
balls.append (new_ball)
def run_balls () :
turtle.tracer (0 , 0) # don't render to screen unless instructed
turtle.hideturtle () # don't show the turtle icon
turtle.penup () # default turtle state is not drawing
turtle.onkeypress (add_ball , 'space') # register an event handler
turtle.listen () # activate event handlers
while True : # the main loop
for ball in balls :
ball.update_pos (timestep)
draw_scene ()
time.sleep (timestep)
No official reading assignment, but f.y.i. chapters 15-17 are about classes in Python.
Come to lab on Thursday.
Complete Homework 11.