Overview
In this lecture we will continue our discussion of classes and object-oriented
programming (OOP) in Python, by extending our Shape
class with a lot of new methods.
Topics
-
Review basic OOP terminology
-
Extend the
Square
class by adding methods
Definitions and terminology
Recall from last time: An object is a data value that contains
-
attributes (data items)
-
methods (functions that act on the object)
Technically, methods are a special kind of attribute: they are attributes which happen to be functions ("attribute" is a Python term, not a general OOP term). We will usually refer to something as an "attribute" to mean a non-function attribute (data attribute) as opposed to a method. Data attributes are also often called fields for short. |
A class is a description of what an object is, and contains the definition of all the methods of an object. [1] Every object is an instance of some class.
Classes and objects
Every object which is an instance of the same class normally contains
-
the exact same methods
-
the exact same data attribute names
Although the data attribute names are the same for all instances of that class, the values corresponding to a given data attribute name are generally different for different objects. [2] We can think of an object as a "bag of data attributes with methods".
Constructor methods
Every class has a constructor method (or
constructor for short) called __init__
The constructor is called in order to create a new object of the class.
The data attributes of an object are normally initialized in the constructor,
though the values can be changed later.
(You can also add more attributes later,
though this is generally considered to be bad practice.)
You call a constructor by using the class name as if it were a function.
For instance, for a class Thing
where the constructor method __init__
takes one argument other than self
,
you would create new instances of the Thing
class
by calling the constructor like this:
t = Thing('Godzilla')
This creates a new (empty) Thing
object,
then calls the __init__
constructor method
with the new object as the self
argument to the constructor,
along with whatever other arguments the constructor needs.
Unlike some object-oriented languages (*cough* Java), a Python class can only have a single constructor. However, you can have optional and/or keyword arguments in the constructor, so this normally isn’t a problem.
Methods
Method definitions look like regular Python function definitions,
except that they are located inside a class
statement.
Method definitions have a first argument called
self
which represents the object being acted on.
The name self
is a convention, not a keyword.
(You could call it something else, but nobody does.)
Inside a method, you can access the attributes of
the current object through the self
argument using the dot syntax.
So an attribute called x
could be accessed by writing self.x
.
When methods are called, the method call is
missing the first (self
) argument;
that argument goes before the dot (.
) in the dot syntax.
So if you have a class Thing
with an instance
t
and a method called get_value
,
then to call the get_value
method you would write:
t.get_value()
Graphics example
There’s more to know about classes and objects, but that’s enough for now. Let’s recap the graphics example we discussed last time and extend it.
We want to create objects to represent shapes
we can draw on tkinter
canvasses (e.g. squares, circles).
We have been defining a Square
class to help us generate Square
objects.
Here’s what we have so far:
class Square: """Objects of this class represent a square on a tkinter canvas.""" def __init__(self, canvas, center, size, color): """Initialize a `Square` object.""" (x, y) = center x1 = x - size/2 # upper left y1 = y - size/2 # coordinates x2 = x + size/2 # lower right y2 = y + size/2 # coordinates self.handle = \ canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline=color) self.canvas = canvas self.center = center self.size = size self.color = color
Let’s build up our Square
class method-by-method
to make our Square
s more powerful.
We’ll put this code in a module called Square
(i.e. in a file called Square.py
)
At the bottom of the file we’ll have code to
create the root object, the canvas, and one or
more Square
s.
Here it is, with some parts omitted:
"""<module docstring>""" from tkinter import * class Square: """<docstring>""" def __init__(self, canvas, center, size, color): # see above # ... other methods ... if __name__ == '__main__': root = Tk() root.geometry('800x800') canvas = Canvas(root, height=800, width=800) canvas.pack() # Create a single red square of side length 50 pixels # centered at pixel coordinates (100, 100). s1 = Square(canvas, (100, 100), 50, 'red')
python -i
Our Square class only has a constructor so far. We can run our program and test it interactively by typing:
$ python -i Square.py
at the terminal prompt ($
).
[3]
The -i
tells Python that you want to run the module interactively.
It’s just like running python3 Square.py
, except that
after it’s done you go back into the Python shell.
When you do this, the program runs,
displays a canvas with a red square,
and you get the prompt:
>>>
At this point, you can play around with the objects you’ve created.
>>> s1 <__main__.Square object at 0x104fd0d60>
Let’s look at the Square
object’s fields:
>>> s1.center (100, 100) >>> s1.size 50 >>> s1.color 'red'
The Square
object stores these values,
so every method of Square
will be able to access them
through the self
argument.
Changing color
What if we want to change the color of a square?
>>> s1.color = 'blue' >>> s1.color 'blue'
We’ve successfully changed the color field of the square. But if you run this code, you’ll see that the square has not changed color! Why?
To actually change the color of an object
representing a shape on a canvas,
it’s not enough to just set the color
field to a different value.
(That information is not communicated to the canvas object that actually
is in charge of the square’s color.)
Instead, we have to call a canvas method called itemconfig
on the handle of the shape:
>>> canvas.itemconfig(s1.handle, fill='blue', outline='blue')
This will change the square’s color to blue.
This is tedious; you have to get the handle from square s1
,
and you have to separately specify the fill and the outline color,
even though we always want them to be the same.
Let’s write a method on Squares
to do this all at once:
1def setColor(self, color):
2 """Changes this object's color to 'color'."""
3 self.canvas.itemconfig(self.handle, fill=color, outline=color)
4 self.color = color
Line 3 changes the color of the square on the canvas.
Line 4 stores the new color into the Square
object.
With this method defined, we can simply do this:
>>> s1.setColor('blue')
and the color of the Square object s1
on the canvas turns to blue!
And also:
>>> s1.color 'blue'
So the new color is recorded in the Square
object s1
.
Changing size: the set_size
method
What if we want to change the square’s size? Let’s try something dumb:
>>> s1.size 50 >>> s1.size = 100 >>> s1.size 100
By now, you shouldn’t be surprised that this doesn’t change
the size of the square on the canvas.
To change the size of a shape on a canvas,
we have to call a canvas method called coords
on the handle of the shape:
>>> canvas.coords(s1.handle, x1, y1, x2, y2)
x1
and y1
are the (new) coordinates of the upper-left-hand corner.
x2
and y2
are the (new) coordinates of the lower-right-hand corner.
We need to compute x1
, y1
, x2
, y2
given the square’s center coordinates
and the new size
value.
We saw how to do this in the constructor (the __init__
method):
(x, y) = center x1 = x - size/2 y1 = y - size/2 x2 = x + size/2 y2 = y + size/2
Let’s write a set_size
method to change the size of a Square
while keeping the center position fixed:
def set_size(self, size): """Change the size of this square.""" (x, y) = self.center x1 = x - size/2 y1 = y - size/2 x2 = x + size/2 y2 = y + size/2 self.canvas.coords(self.handle, x1, y1, x2, y2) self.size = size
Note that we record the new size as self.size
.
Let’s call this method on square s1
:
>>> s1.set_size(120)
Now s1
is 120 pixels on each side.
Changing size: the scale
method
Let’s also write a scale
method to change the size of a Square
by some scaling factor
(while keeping the center position fixed, of course):
def scale(self, scale_factor): """Scale the size of this square by a scaling factor.""" self.set_size(self.size * scale_factor)
Now, if we do this to square s1
:
>>> s1.scale(2.0)
the square will grow to twice its original size.
This method is particularly interesting,
because we called another method of the same class
to do all the real work.
Since set_size
already knows how to set the size of a Square
to any particular size,
there is no point in repeating that code.
Instead, we simply calculate the desired size
(which is trivial) and call the set_size
method.
This is the D.R.Y. principle in action again.
Re-use methods inside other methods when it will make the code more concise. |
More methods!
Let’s add more methods! We want to be able to
-
move squares relative to their current position
-
move squares to some absolute position
-
lift squares to the top of the "stacking order"
-
delete squares
Moving squares
Canvases have a move
method which can move a shape on a canvas
given the handle of the shape.
We would like to be able to move our squares around to new positions.
Let’s define a move
method which calls the canvas move
method.
Using the move
method on squares
will be much less tedious than using the move
method on canvasses.
Here’s our first try:
def move(self, x, y): """Move this square by (x, y).""" self.canvas.move(self.handle, x, y)
This will work, but it’s not enough.
The square
needs to know its location,
which is stored in the center
field of the Square
object.
We’ve changed the location,
so we must also update the object’s center
field.
def move(self, x, y): """Move this square by (x, y).""" self.canvas.move(self.handle, x, y) (cx, cy) = self.center self.center = (cx + x, cy + y)
If we call this method on square s1
:
>>> s1.move(50, 150)
the square will move 50 pixels in the X (horizontal) direction and 150 pixels in the Y (vertical) direction.
Move to absolute location
The move
method will only move a square relative to its current location.
We might want to move a square to an absolute location on the canvas
e.g. to point (350, 450).
[4]
Let’s define a method to do that. We’ll call it move_to
.
The trick in defining this method is this:
if we knew the difference between
our current location and the target location,
we could call the move
method with those numbers
(difference in X position, difference in Y position).
As we mentioned above with the scale
method,
when you have some basic methods defined,
you can often define other methods in terms of them.
This is a good thing, because you need to write less code overall.
Here’s our first version:
def move_to(self, x, y): """Move this square to the location (x, y) on its canvas.""" (cx, cy) = self.center dx = x - cx dy = y - cy self.canvas.move(self.handle, dx, dy) self.center = (x, y)
Note that we need to record the new (x, y)
position in the
center
field of the square object.
If we call this method like this:
>>> s1.move_to(200, 200)
then the square s1
is now centered on the point (200, 200)
of the canvas.
We can simplify this code by calling the move
method
instead of self.canvas.move
, leading to the final version:
def move_to(self, x, y): """Move this square to the location (x, y) on its canvas.""" (cx, cy) = self.center dx = x - cx dy = y - cy self.move(dx, dy)
Change the stacking order
When two shapes on a canvas overlap, only one can be visible in the overlapping region. The canvas object keeps track of "what is above what". This is called the stacking order of the canvas. Shapes on top of the stacking order cannot be covered up by other shapes. Every shape on a canvas has a place in the stacking order (even shapes that have no other objects overlapping with them).
Sometimes, we want to manually "lift" a canvas shape
to the top of the stacking order
in order to make the entire shape visible.
The canvas object defines a lift
method to do this.
Let’s define such a method for our Square
s:
def lift(self): """Lift this square to the top of the canvas stacking order.""" self.canvas.lift(self.handle)
That’s all it takes! Now we can do (for square s1
):
>>> s1.lift()
and s1
will pop up above any other shapes that overlap it.
Deleting squares
Let’s finish with an easy method: delete
.
This method will remove a shape from a canvas.
However, it will not remove it from the Python program!
(We’ll see how to do that later.)
Here’s the method:
def delete(self): """Remove the square from the canvas.""" self.canvas.delete(self.handle)
That’s it! Now, when this method is called, the square
disappears from the canvas.
However, if we’re in the Python interpreter
(for instance, because we used python -i
to start the program),
we can see that the Square
object is still there:
>>> s1.delete() [square disappears from canvas] >>> s1 <__main__.Square instance at 0x109a11d30>
The Square
object is still there!
How do we make the object s1
go away?
To delete s1
entirely we need to use the del
operator:
>>> del s1 >>> s1 NameError: name 's1' is not defined
It would be nice if we could combine these two kinds of "deletions" into one method.
Python allows objects to define a special method called __del__
It is called whenever del
is applied to an object
(i.e. del s1
for the square s1
).
It allows us to do additional clean-up before the object is deleted.
(In the object-oriented programming world,
this kind of method is called a finalizer.)
Let’s define it:
def __del__(self): """Delete this square.""" self.delete()
That’s it! Now, when a square s1
is visible and we type del s1
,
not only will the square disappear from the canvas,
but the object s1
will disappear from Python!
Some interesting points about this method:
-
We are calling one method (
delete
) from inside another method (__del__
). -
This method doesn’t replace what
del
used to do; it gets run just beforedel
destroys the object.
Summing up
Now our Square
objects are pretty functional.
We can create them, move them,
change their color,
change their stacking order,
change their size,
and delete them.
If we thought up some other functionality
that we’d like to have
(say, the ability to rotate them around their centers),
we could define a method to do that too.
By making Square
s into objects
instead of just internal components of tkinter
canvasses
identified by integer handles,
we have changed the crude and annoying-to-write interface
of the canvas methods that manipulate shapes
into something much more pleasant and intuitive.
Later, we’ll take advantage of this to write programs
where we will manipulate large numbers of shapes
with a small amount of code.
[End of reading]