I Wanted to be a Lumberjack (logging)

I Wanted to be a Lumberjack (logging)

Barber:     All right … I confess I haven’t cut your hair …  I didn’t want to be a barber anyway. I wanted to be a lumberjack. Leaping from tree to tree as they float down the mighty rivers of British Columbia . . . (he is gradually straightening up with a visionary gleam in his eyes). The giant redwood, the larch, the fir, the mighty Scots pine. (he tears off his barber’s jacket, to reveal tartan shirt and lumberjack trousers underneath; as he speaks the lights dim behind him and a choir of Mounties is heard, faintly in the distance)

When something goes wrong with a program, it is natural to try to diagnose the problem by using the print statement to check the value of variables.  As your program grows in size however, this begins to get unwieldy, if only because the output of print ends up in the console and you lose it to the autoscrolling.  Python offers another way to record these messages – the logging module.  In its most most basic form:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.DEBUG)

if __name__=="__main__":
  logging.debug("This is a test entry")

If you save this to a file (p4kLogging.py) and run it, it should:

  • create a file called “test.log” (we could have called it something else if we wished (this is filename =fn)
  • if such a file already exists, that file is deleted and a new, empty one created on top of it (this is the filemode = “w” part)

Let’s read the file now from the Python console:

>>> fn  ="test.log"
>>> f = open(fn,'r')
>>> print f.read()
DEBUG:root:This is a test entry

>>> f.close()

This gives us the message (“This is a test entry”), as well as what logging status it has been given (DEBUG) and what function made the log (root – or the main part of the program)

Let’s try the same program again, but this time we’ll set the logging level to WARNING:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.WARNING)

if __name__=="__main__":
  logging.debug("This is a test entry")

This time, when we print the contents of the file:

>>> f = open(fn,'r')
>>> print f.read()

>>> f.close()

It’s empty! This is because we configured the logging to only show messages which were of WARNING or higher priority.  Since DEBUG is of lower priority it was filtered out.  Most excellent! Imagine if these were print statements instead.  Once the program was finished/or the problem debugged, you need to go back and either remove or comment out all of the print statements (leaving important ones alone – so you can’t necessarily just use search and replace). Now, instead, we simply turn all of them off by a simple change of the logger configuration.  However, it gets better.

The output we initially saw was a little pedestrian. We can configure the logger to provide heaps more information.  Here’s a slightly longer program with a more involved logging parameter:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.DEBUG, format= "%(levelname)s %(asctime)s %(funcName)s @%(lineno)d %(message)s")

def functionA():
  logging.debug("We're in function A")

def functionB():
  logging.debug("We're in function B")

if __name__=="__main__":
  logging.debug("This is a test entry")
  functionA()
  functionB()

Look at what we get now when we run the program:

>>> f = open(fn,'r')
>>> print f.read()
DEBUG 2012-10-22 16:26:15,789 <module> @13 This is a test entry
DEBUG 2012-10-22 16:26:15,789 functionA @7 We're in function A
DEBUG 2012-10-22 16:26:15,789 functionB @10 We're in function B

>>> f.close()

Now, we get a bunch more info, including the time and date of the logging message (down to milliseconds).  Not only that, we get the name of the function from which the function call was made and the line number in the program where the function call was made.

Advanced stuff

Knowing the line number in particular is pretty useful.  For example, you can filter the log file for references to that specific line number eg (this only works on systems with grep installed of course):

>grep \@7 test.log
DEBUG 2012-10-22 16:26:15,789 functionA @7 We're in function A

You can even use it to open up your file at the exact line number:

>vi p4kLogging121022C.py +7

This, of course, is only if you’re using the vi editor, but other editors should have “go to line” functions which can also be used.   This is much easier than hunting through your text file trying to find where the logging call was made.

Final comments

Different levels you will typically use are logging.debug, logging.warning and logging.error.  The logging function you use will log whatever string is passed as an argument:

logging.debug("It's this bit which will print")

You can use string formatting here as well to log variable values:

logging.warning("The value of x is %s"%(x))

It is good practice to use a logger for your debugging messages.  If you have a short program it may be easier to just use print statements.  However, as your programs grow, you will definitely need to do logging instead.

More info: Logging how to, logging docs

Slider Spliner

Back to the photo sequence and music. Each photo is on the screen for only two seconds, and in between each there is a click as of a slide projector changing or even the sound of the shutter of a camera. The photos show in sequence: Anthony Barber, Katy Boyle, Edgar Allan Poe, a loony head and shoulders. He has ping-pong ball eyes, several teeth blocked out, a fright wig and his chest is bare but across it is written ‘A Loony’, Reginald Maudling, Tony Jacklin. A buzzer sounds.

In the last tutorial I used the term “quadradic Bezier curve” (which is a particular sort of way of drawing a curved line through three points).  A more generic name for this way of drawing curves is “spline”, named after a strip of flexible material used by draftsmen to draw curves, also now called flexicurves).   In this tutorial we’re going to learn about the Scale widget (but it looks like a slider) and use it to show how a computer draws those smooth curves.

Our starting point is the canvasCurve program from the previous tutorial, but we’ll jettison the mouse motion stuff, make it a bit larger and just draw a fixed, rather large, unhappy looking spline:

 

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12
    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
      boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
      # mixed + and - because y runs from top to bottom not bottom to top
      self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

To this we will add our Scale widget/slider bar.  The Scale widget can be configured in a variety of ways.  In this case we’ve passed it from_ (note the trailing underscore) and to parameters (self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)).  These are the numbers that the widget will range from and to respectively.  If you grab and move the slider you will see its values change, but nothing happens to the curve:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
    boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
    # mixed + and - because y runs from top to bottom not bottom to top
    self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

Exercise:  slide the slider from left to right and confirm that it runs over all of the values from from_ to to.

Exercise: try different values for from and to.  Does the slider change its appearance?  If to is much greater than from_  does the slider miss some values in between when you slide it?

I could hook up a listener here to track whether the slider has changed, but I’m just going to add a button.  When you press the button it gets the slider’s value and updates the drawing based on that value:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.updateButton = Button(text="Update!",command = self.update)
    self.updateButton.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
    boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
    # mixed + and - because y runs from top to bottom not bottom to top
    self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

  def update(self):
    # important bit here is to "get" the value in the slider
    self.splinesteps = self.slider.get()
    self.drawTheSpline()

Canvassing()
mainloop()

Exercise: Click the button for different values of the slider.

So, what’s happening here?  When Tkinter draws a smooth line (using create_line) it doesn’t actually draw a curved line.  Rather, it draws a number of much shorter straight lines.  You will notice that the create_line entry has a parameter called splinesteps.  This parameter tells Tkinter how many of these shorter straight lines should be drawn when approximating the curve.  If no number is passed to this parameter, it defaults to 12.  You will see that the spline becomes increasingly more refined as this number is increased.  You should also note that there is a great deal of improvement in the curve when these numbers are small, but much less improvement when the numbers are large (do the next exercise).

Exercise:  See that there is a big change in the curve when this number changes from 1 to 2, and 2 to 3, but very little improvement from 11 to 12 (or even from 12 to 20).

Finally, I want to point out that, since self.slider is a widget, it doesn’t have a value as such so an assignment such as someVariable = self.slider doesn’t make sense to Python, although it is perfectly logical for us. Rather, since self.slider is a Tkinter Scale Widget, it is really an instance of the Scale Class.  This means it has a lot of different attributes and methods (try print dir(self.slider)).  To get the value of the slider at any point, you need to use its get() method.  This is a common pattern for Tkinter widgets so you’ll need to get() used to it.

Quadratic Bezier Curves

An open field. A large hamper, with an attendant in a brown coat standing behind it. The attendant opens the hamper and three pigeon fanciers, (in very fast motion) leap out and run off across the field, wheeling in a curve as birds do.

Or, in other words, smooth lines.  This is not the tutorial I had intended to write, but the tutorial I had intended to write is too long in coming.  I have been working on a stick figure program in Python and need to prototype drawing curved lines using Tkinter. I figured I may as well incorporate it into a tutorial.

A Loose End

At the end of the Being Animated tutorial I set some exercises – I hope you’ve done them by now.  One of the questions I asked was why I chose a width of 21 pixels and not 20?  There is no magic in the number 21 per se.  Rather, the issue is whether or not the number is an even or an odd number.  Harking back to our discussion about pixels you should realise [sic] that pixels on the screen are not like marks on a ruler.  Marks on a ruler take up no space pixels, on the other hand, do.  If you chose an even number of pixels for something you want to centre [sic] somewhere the centre pixel will be at the centre.  Then you will have a different number of pixels on either side.  To take an extreme example, if it was two pixels wide, then one pixel would be at the mouse’s location, and one would be left hanging.  There would not be a way to balance it.  However, if it was three pixels wide, one could be at the mouse position, and one could be on either side.

Introduction

In the Being Animated tutorial we also saw how to draw a line on a Tkinter Canvas Widget.  In this tutorial we’re going to look at how to draw a curved line. I have used the code from canvasLine2B.py in that tutorial as a starting point for the code below – save this to canvasCurve1.py:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 200
HEIGHT = 200
CENTREX = WIDTH/2
CENTREY = HEIGHT/2

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.onMouseMotion)
    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4),
		    (CENTREX, CENTREY)
		    ]

  def onMouseMotion(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    self.canvas.delete(self.line) # first time through this will be None
    # but Tkinter is ok with that
    self.line = self.canvas.create_line(self.points, (event.x,event.y), width=2, fill = "yellow")

Canvassing()
mainloop()

You can see that I’ve:

  • removed the magic number 100 from the code, and replaced it by WIDTH and HEIGHT – capitalised variable names to indicate that they are (or should be) global constants;
  • added a list called self.points which, at the moment, contains two points;
  • change the name of the second method to onMouseMotion (because it’s what is done when there’s a movement of the mouse);
  • increased the width of the line so it is a little easier to see;
  • changed the drawing code.  Now the two points above and the current position of the mouse (event.x, event.y) are fed into create_line;
    and
  • added a title to the window.

This gives us something that looks like this (except that the second line segment follows the mouse, something this still image doesn’t do justice to):

Exercise: Try some other values of WIDTH and HEIGHT to verify that the program still works.

Exercise: Try adding more entries to self.points (in the form (x,y) with commas between them all, but none after the last one)

Drawing nodes

To demonstrate that there are three points involved I am going to add some code to draw a small circle around each point, and moving the one under the mouse pointer with the mouse:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 200
HEIGHT = 200
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.onMouseMotion)
    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4),
		    (CENTREX, CENTREY)
		    ]

  def onMouseMotion(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)
    # deleting everything every time is inefficient, but it doesn't matter for our purposes.
    for p in self.points:
      self.drawNode(p)
    p = (event.x, event.y)  # now repurpose p to be the point under the mouse
    self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR)
    self.drawNode(p)

  def drawNode(self, p):
      boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
      # mixed + and - because y runs from top to bottom not bottom to top
      self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

This gives something similar to the picture above, but with the nodes clearly shown:

Magic Pixie Dust

Once we’ve done this, drawing a curved line is a snap.  We simply add a new parameter, smooth, to the create_line method call:

    self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR, smooth = True)

Now you can see the curve clearly shown as well as where the points are:

A Short Aside – Some Python Minecraft Stuff

The blog has been on holiday for many weeks now, following my own holiday. I am looking to get back into it over the next little while. In the meantime I have done some Google searches for Minecraft related programs written in Python that might be of interest/use.  I have not run any of them myself though, so reviews welcome.
Minecraft Screenshot Viewer in wxPython by Davy Mitchell. This viewer is written using wxPython, which is a Tkinter substitute.  You may need to install wxPython separately for it to work.  The code itself is here.

A variety of Minecraft scripts for editing save files. The site’s maintainer, Paul Spooner has asked for feedback on the scripts (see comments below) so get in contact if you have any.

MCSuperServer, which will automatically start your server for you when someone tries to connect.

Handle - a front end for bukkit it allows you to easily automate mundane tasks associated with running a server.

Homework: Open at least one of these in a text editor/online and try to understand how the code works/does what it does.

Being Animated

CAPTION: ‘AN APOLOGY’
Voice Over     The BBC would like to apologize for the constant repetition in this show.
    DIFFERENT CAPTION READING: ‘AN APOLOGY’
Voice Over     The BBC would like to apologize for the constant repetition in this show.
    ANIMATION: the ‘five frog curse’.
Cut to the five Gumbys standing in a tight group.

In the last tutorial we looked at the Canvas widget from Tkinter and we also had a look at the concept of cartesian coordinate systems. In our example we could identify specific pixels by counting across and down from the top left of the canvas (ordinarily cartesian systems count left and up).  We also bound the motion event to the canvas and used it to display the current coordinates of the mouse in the canvas.  The reason we did this is because to draw anything on the canvas you need to know the coordinates of where it is to be drawn.

Part of the homework was to identify what it was about the readout which didn’t make sense.   I didn’t see any comments on the tutorial, so I assume you’re baffled by what didn’t make sense.

Answer: if you move the mouse around enough, you can get pixel positions of (0,0) and (101,101).  This means that there are 102 pixels (from 1 to 100 is 100 plus one for each of 0 and 101 gives 102).  This is odd because when we set up the program we specifically said we wanted both the height and width to be 100 pixels.   Where are these two extra pixels in each direction coming from?  If you have a screen magnifier you can see it yourself, but you can also see it on the enlarged picture from the previous tutorial.  There is a white border around the blue canvas area:

It is this border where the extra pixels come from.

The canvas widget has a variety of methods for drawing stuff on the canvas area.  To use these methods you need to provide start and end coordinates.  Here is a yellow line drawn in the centre of the canvas from top to bottom:

To create this I added:

    start = (50,0)
    end = (50,100)
    self.canvas.create_line(start, end, fill = "yellow")

to the earlier code after the self.canvas.bind line.  The thing to note here is the use of the coordinate system we discussed.

Exercise 1: swap the values of start and end.  What difference does this make?  Keep end static and try some different combinations for start.  Try to predict what it will look like before you run the program.

Exercise 2: change the code so that the line runs left to right through the middle rather than up and down.

Our next task is to try to animate this line.  We already have code to track mouse movements over the canvas, so let’s repurpose it to draw a line where the mouse pointer is (removing the code for the line in the middle):

# -*- coding: utf-8 -*-
# canvasLine2.py
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)

  def updateCoordinates(self,event):
    self.readout.config(text = formatString%(event.x, event.y))
    start = (event.x, 0)
    end = (event.x, 100)
    self.canvas.create_line(start, end, fill = "yellow")

Canvassing()
mainloop()

If you run this and move the mouse around you will see something like this:

What’s going on here is that we are drawing new lines, but we aren’t erasing the old ones.  The computer, being “incredibly fast, accurate and stupid” doesn’t realise that we’re only interested in having one of the lines we’ve drawn present at any one time. We could recycle a single line moving it around, but that would require me to explain the concept of deltas and offsets, which you’ll need to look up for yourself.  Instead, we’re going to delete each line and draw a new one each time the mouse is moved.

# -*- coding: utf-8 -*-
#canvasLine2B.py
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)
    self.line = None

  def updateCoordinates(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    start = (event.x, 0)
    end = (event.x, 100)
    self.canvas.delete(self.line) # first time through this will be None
    # but Tkinter is ok with that
    self.line = self.canvas.create_line(start, end, fill = "yellow")

Canvassing()
mainloop()

Now when you run the code you should get a single vertical line which follows your mouse pointer as it moves over the canvas.  Hey presto! You’ve just done your first animation.  Animation on computers involves deleting stuff one the screen which is out of date (or “painting” over it) and then replacing it with new stuff.  The value of self.line here is just an integer.  This is a reference that Tkinter uses to identify the objects it has drawn on the screen.

Exercise: add a print statement to print out the value of self.line for each call to updateCoordinates()

Exercise: change the code so that instead of a vertical line there is a cross hair (with a horizontal line running across the canvas) following the mouse pointer.  Hint: you need to draw two lines

Extra points: make the cross hair only 21 pixels wide centred on the mouse pointer.

Extra extra points: why 21 pixels and not 20?

Canvassing

Cut to Art Gallery. A large sign says: ‘Italian Masters of the Renaissance’. Two art critics wandering through. They stop in front of a large Titian canvas. The canvas is about ten foot high by six foot wide.
First Critic     Aren’t they marvelous? The strength and boldness… life and power in those colours.
Second Critic     This must be Titian’s masterpiece.
First Critic     Oh indeed – if only for the composition alone. The strength of those foreground figures … the firmness of the line…
Second Critic     Yes, the confidence of the master at the height of his powers.

In this tutorial we are going to have a look at a new Tkinter widget called the Canvas.  A canvas widget, is, somewhat like a real canvas, something that you can throw paint or other stuff all over.  If you want to program games or animation you need to have an understanding of coordinate systems and computer animation.  A basic understanding of coordinates is the main aim of this tutorial.  In the process we will also incidentally cover another type of event – Exercise – find the new event. What is it called?

For this tutorial I want you to create a new file in your python4kids directory called canvassing.py

# -*- coding: utf-8 -*-
from Tkinter import *

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=300, height=200, bg = "blue")
    self.canvas.pack()

Canvassing()
mainloop()

This code, when run, should give you something that looks like this (click the close widget to close):

The default background is a sort of white, and I thought you might not be able to tell against the WordPress site, so I used blue instead.  Isn’t Python clever to know what blue is?  I hope you can also tell that if you put different numbers in for width and height you’d get a different looking rectangle.

Exercise: Change the height or width parameters (but not both) and see what you get, try a couple of different values.  Now try changing both the height and width parameters.

It’s important that you do the exercise as it demonstrates a relationship between the numbers and the size of the grid.  In our example, we have a rectangle which is wider (300) than it is tall (200).  In fact, if you were to look closely and count carefully, you would find that there are literally 300 blue dots along the bottom of the rectangle and 200 blue dots running up each side.

Exercise: Change width to be 10 and the height to be 1:

    self.canvas = Canvas(width=10,height=1, bg = "blue")

now, go find a magnifying glass and count the dots in the canvas.

Each of those dots is called a pixel (short for picture element).  Our original canvas was 300 pixels wide and 200 pixels high.  It had a total of 60,000 (ie 300×200) individual pixels!  Computers display their information by changing each of those individual pixels. Now, let’s make another tiny canvas and draw two tiny (1 pixel wide) lines on our new tiny canvas (save this to canvassing10x2.py):

# -*- coding: utf-8 -*-
from Tkinter import *

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=10,height=2, bg = "blue")
    self.canvas.pack()
    self.canvas.create_line(1,1,2,1,fill="red")
    self.canvas.create_line(5,2,6,2,fill="yellow")

Canvassing()
mainloop()

This should give you a window that looks something like this (grab an edge and expand it to get a close widget):

With this you should just about be able to see the two pixels we coloured in (don’t do this generally by the way, Tkinter is not built for doing pixel operations).  The first is the red pixel in the top left corner.  The second is the yellow pixel on the bottom in the middle.  In case you don’t have a magnifying glass, here’s a photo I took of my screen which shows it a little better:

The pixel boundaries are obvious in this photo (see the grid lines?).  If you look hard you can count the pixels (the photo is a little too good because it shows the subpixel array* used by my monitor to create the colours). For good measure here is the original picture blown up 4x:

So, why is the red in the top left and the yellow in the bottom middle?  Well, the canvas is  10 pixels wide and is 2 pixels high. So you can identify each of the pixels by whether they’re on the top row or bottom row, and how far along they are.  In this case, we created a red line starting at 1,1 and ending at 2, 1.  In each case the first number is how far along from the left and the second number is how far down from the top.  So 1,1 is the first pixel on the first line while 2,1 is pixel two on line 1 (if you look closely we’ve actually ended up with a black pixel at pixel 2.

In the second case (5,2,6,2) we drew a line from pixel 5 on line 2 (5,2) to pixel 6 on line 2 (6,2).  Since 5 is midway between 1 and 10 it looks like it’s in the middle.

So, what’s all this about?  Well anything to do with manipulating a canvas is about coordinates. That is, what pixel on what line.  Larger canvases just mean more pixels to play with.  Here we had 20 pixels (ten pixels each line on two lines) but our original example had 60,000.  A computer screen with a resolution of 1024 x 768 (which, as at June 2012, Wikipedia alleges represents about 20% of web users) has three quarters of a million pixels (786,432).   The bottom line is that if you understand how coordinates work, then you’re already a good way there to doing computer graphics and animations.

In the last example we use an event to print out the coordinates of the mouse as you move it around the canvas (save this to canvassingCoordinates.py):

# -*- coding: utf-8 -*-
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)

  def updateCoordinates(self,event):
    self.readout.config(text = formatString%(event.x, event.y))

Canvassing()
mainloop()

Homework: 1. There’s something about the readout which doesn’t make sense.  What is it?

2. Explain what is happening here.  How does the label readout work?

* On my LCD each “pixel” is actually a group of three smaller pixels, one red, one blue, one green.

Being Exceptional

Lady     Yes this looks the sort of thing. May I just try it?
Assistant     Certainly, madam.
    The lady presses button and a sheet of flame shoots out across the hall.
Lady     Oh! Sorry! So sorry! (she is happy though) Yes that’s fine.
Assistant     Is that on account, madam?
Lady     Yes.

Apparently, in Python, it is easier to ask for forgiveness rather than seek permission.   That is to say, the normal approach when writing Python code is to assume that what you are trying to do will work properly.  If something exceptional happens and the code doesn’t work the way you were hoping, then the Python interpreter will tell you of the error so that you can handle that exceptional circumstance.  This general approach, of trying to do something, then cleaning up if something goes wrong is acronymically called EAFP (“easier to ask for forgiveness than permission.  Here is a (somewhat silly) example:

>>> a= 1
>>> b="2"
>>> a+b
Traceback (most recent call last):
File "", line 1, in
TypeError: unsupported operand type(s) for +: 'int' and 'str'

What has happened is that we have tried to add a word (ie the string “2”) to a number (the integer 1).  Addition doesn’t make any sense in this circumstance, so Python “throws” an “exception”.  In this case the exception is called a TypeError.   The problem with this is that when an exception is raised, then unless the program deals with the exception, the Python interpreter will step in, stop the program and print an error message like the one above.

Python deals with this by the try/except structure. You try to do the statements in the first block of code, and if there is an exception (that is, you failed when you tried), then you do the statements in the second block of code.

>>> try:
...    print a+b      # this is the first block of code and can be multiple statements
... except TypeError:
...    print "Can't add those things together!" # this is the second block of code
...
Can't add those things together!

Can you see that no exception was raised here?  As far as the Python interpreter is concerned, everything ran smoothly.  The program tried to print a+b, and, in doing so, tried to work out what a +b is.  However, it failed because a is a number and b is a string.  Because it failed nothing got printed in the first block of code.  Also because the specific failure was a TypeError, the second block of code was run.

A short code snippet can show you how this works:

>>> c = [1,"2",2,3,4,5]
>>> sumOfc = 0
>>> # Try Number 1 - this will fail
>>> for number in c:
...     sumOfc += number
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
>>> # Try Number 2 - this will work
>>> sumOfc = 0  # need to reset the sum
>>> for number in c:
...     try:
...        sumOfc += number
...     except TypeError:
...        pass
...
>>> sumOfc
15

In the first try, the interpreter ran into the string “2” and didn’t know what to do, so gave up.  In the second try, the interpreter knew exactly what to do if there was a problem with the addition (pass – or, in other words “do nothing”[see note 1]) so it went through the whole of the array adding up the things that were numbers without complaining.

Of course, you don’t use try/except blocks around everything.  You will usually have an idea about the parts of the code where something goes wrong.  You can put that code in a try/except block, with code to deal with a failure.  What you shouldn’t ever do is this:

>>> sumOfc = 0
>>> c = [1,"2",2,3,4,5]
>>> for number in c:
...     try:
...        sumOfc += number
...     except:   # <- Don't do this!
...        pass
...
>>> sumOfc
15

It is not entirely obvious what is wrong here – the code works just as well as before.  The only difference is in the except line – there is no exception specified.   The reason that this is bad isn’t that the code doesn’t work well, but, rather, that it works too well.  Every exception will be caught here, not just the one that you were expecting.  This is bad because if some other problem arises in your try block, you’ll never learn about it and your exception code will probably deal with it incorrectly.  You won’t know why your program doesn’t work because the interpreter won’t tell you.

The other approach to dealing with possible errors is called Look Before You Leap (LBYL).  LBYL involves making sure everything is right before you actually do the thing you do the problematic operation.  So, for example, you might check that what you were trying to add was an integer:

>>> c = [1,"2",2,3,4,5]
>>> sumOfc = 0
>>> for number in c:
...    if isinstance(number,int):
...       sumOfc += number
...
>>> sumOfc
15

Here the isinstance() function is another feature of Python introspection.  It tests whether the variable number is an integer (“int”).  So this code doesn’t even bother trying to add the number unless its an integer.  It first checks “is this an integer I see before me?”  If so, it adds it, if not, it ignores it.   Which is fine as far as it goes…

The problem with this is that it’s the wrong way around.  You’re not really interested in whether or not the thing you’re adding is an integer.  Rather, you’re interested in knowing whether addition is meaningful [see note 2].  In fact, this way of approaching things is broken:

>>> c = [1,"2",2,3,4,5.2]
>>> sumOfc = 0
>>> for number in c:
...    if isinstance(number,int):
...       sumOfc += number
...
>>> sumOfc
10

We’ve changed the last number in the array to be 5.2, but this causes it to be ignored because it isn’t an integer – ooops!  Applying the earlier try/except code gives the right result though:

>>> c = [1,"2",2,3,4,5.2]
>>> sumOfc = 0
>>> for number in c:
...     try:
...         sumOfc += number
...     except TypeError:
...         pass
...
>>> sumOfc
15.199999999999999

Well, accurate up to the rounding error…  Our try/except code gave us floating point addition for “free”, where the LBYL failed.  In fact, it’s even more interesting because how it works is dependent on what we define sumOfc to be:

>>> sumOfc = ''
>>> for number in c:
...     try:
...         sumOfc += number
...     except TypeError:
...         pass
...
>>> sumOfc
'2'

Wow! If that didn’t make you giddy with excitement, I don’t know what will.  The same code works if we make sumOfc a string.  Implicitly then we’re asking Python  to join the strings (which is what + means for a string) together and ignore stuff that can’t be joined.   We got this entirely for free from our try/except structure, something that would have needed reworking in the LBYL.  In fact, we could use this structure for any object that has addition defined for it.  It’s this sort of clever which makes Python so good.

Caveat:

Normally the place to use exceptions is where, every once in a while, something out of the ordinary will happen and you have some idea about how it will be out of the ordinary.  In the first case, if it’s not unusual then it’s not exceptional – don’t handle it with exceptions.  In the second case, if you can’t identify how it will be out of the ordinary you can’t deal with it in your except block.

Notes:

1. The point of the except block is to deal with any problems in the try block.  In this case you might try to convert the value to an integer in order that it could be added.

2. Implicitly we’re talking here about breaking interfaces. You, as a programmer, shouldn’t be concerned with what is going on under the hood.  You should be able to rely on the fact that an addition method is defined for an object.  If it is, then  you ought to be able to use it without having to know the internal details of the object.  So in the example here, addition is meaningful between integers and floating point numbers, but addition is also meaningful between two strings.

Follow

Get every new post delivered to your Inbox.

Join 72 other followers