Side Track – Global and Local Variables

… once a week there’s an excursion to the local Roman ruins where you can buy cherryade and melted ice cream and bleedin’ Watney’s Red Barrel, and one night they take you to a local restaurant with local colour and colouring

In the previous tutorial, I used a strange and cumbersome approach to storing the data that I needed to keep track of the image files in the directory, the total number of images and what image we were up to.  Instead of just saving the current image number in a variable called currentImage I instead created a dictionary called indexes and a key called currentImage, then assigned the value to that key.  I did the same thing for totalImages as well.  You might, justifiably, be thinking – why didn’t we just use two variables called currentImage and totalImages?  This tutorial is going to try to explain that.

Let's set up the dictionary using code from the previous tutorial:
>>> indexes = {}  # create a dictionary
>>> indexes['totalImages'] = 17 # 'totalImages is a key in the dictionary
>>> # 17 is the perfectly random number (honest, no number is more random!)
... indexes['currentImage'] = 1

Now let’s assign this value to a variable called currentImage:

>>> currentImage = 1

Confirm what we’ve typed:

>>> indexes
{'currentImage': 1, 'totalImages': 17}
 >>> currentImage
 1

So far, so good.  The problem comes when we try to refer to these in a function.  Let’s define a function which tries to print them:

>>> def printIndexes():
 ...   print indexes
 ...
 >>> printIndexes()
 {'currentImage': 1, 'totalImages': 17}

Now, let’s do function which tries to print the value of currentImage:

>>> def printCurrentImage():
 ...     print currentImage
 ...
 >>> printCurrentImage()
 1

So far we haven’t had a problem.  However, in our slideshow application we needed to increment the value of currentImage, so let’s change our functions to do that:

>>> def incrementIndexes():
 ...   indexes['currentImage']+=1
 ...

Confirm the starting values, run the function, then see what the ending values are after calling the function:

 >>> indexes
 {'currentImage': 1, 'totalImages': 17}
 >>> incrementIndexes()
 >>> indexes
 {'currentImage': 2, 'totalImages': 17}

Now, do the same for the variable currentImage:

>>> def incrementCurrentImage():
 ...    currentImage += 1
 ...
 >>> currentImage
 1
 >>> incrementCurrentImage()
 Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 2, in incrementCurrentImage
 UnboundLocalError: local variable 'currentImage' referenced before assignment

This worked for the dictionary, but it all blew up when we tried to do the same thing with the variable.  Of note is the error message: local variable ‘currentImage’ referenced before assignment.  This error can be avoided by use of the global keyword – but don’t avoid it this way:

>>> def incrementCurrentImage():
 ...    global currentImage
 ...    currentImage +=1
 ...
 >>> currentImage
 1
 >>> incrementCurrentImage()
 >>> currentImage
 2

The reason that the function failed originally is that Python was looking for a ‘local’ variable.  That is, a variable whose name only makes sense while the program is within the function. Local variables serve an important function in allowing you to modularise your code and re-use the same variable names but with different meanings in different contexts.  Thus, in a function which is processing strings the variable currentValue can be doing something different to a variable with the same name called currentValue but in a different function processing (eg) dictionaries.   This is especially true with recursive functions -but don’t expect to see any recursive functions here!

Unless you expressly tell Python otherwise, if a variable is changed inside a function, it is a local variable (if a variable is only read, then Python is happy to assume it’s a global variable – in a sense, it has to be because if it’s only read, it must have got its value somewhere outside the function).  This means it must be initialised within the function (since its name has no meaning outside the function).  If it hasn’t been initialised Python gets confused.  Hence the UnboundLocalError: it gave us.

How about this:

>>> aVariable = 1
 >>> def incrementLocally():
 ...   aVariable = 1
 ...   aVariable += 1
 ...   print aVariable
 ...
 >>> print aVariable
 1
 >>> incrementLocally()
 2
 >>> print aVariable
 1

In this example, despite doing stuff to aVariable inside the function, the variable called aVariable that we started with has been unchanged.  Now try this:

>>> def incrementLocallyAgain(aVariable):
 ...    aVariable += 1
 ...    print aVariable
 ...
 >>> print aVariable
 1
 >>> incrementLocallyAgain(aVariable)
 2
 >>> print aVariable
 1

The same thing happened, even though we passed in aVariable as an argument to the function.   Within the function aVariable was a local variable.  It got a value when the function was started and that value was assigned locally.  Because local variables must be assigned a value before they are referenced it also follows that each time a function is called, its local variables are reset or reinitialised.  Indeed, each time the function is called, a whole new piece of memory is allocated to the function to run in.  This also means that local variables are not remembered from one invocation of the function to another (and even if they were, the need to initialise the variable would end up forgetting the old value anyway).

So, why did it work for a dictionary?  This is because dictionaries provide a level of indirection to the values stored in them.  The local functions never tried to assign a value to the name used by the dictionary, they only ever tried to assign a value to a key within the dictionary.  That is, they never said

indexes = 5

Rather, they said

indexes['someKey']=5.

So Python only looks up the dictionary.  When it doesn’t find it locally, it assumes it (the dictionary) is global.  From there it can access and change the values of keys in the dictionary.

You can see what values are local and global using two specific functions called locals() and globals() respectively.  These functions return a dictionary with the names and values of local and global variables respectively.  They are also additional examples of Python’s introspection as we discussed in our earlier tute.

>>> def incrementLocally():
...    aVariable = 1
...    aVariable += 1
...    print aVariable
...    print 'locals = ',locals()
...
>>> aVariable = 6
>>> incrementLocally()
2
locals =  {'aVariable': 2}
>>> aVariable
6

Usually you won’t need to worry about whether something is local or global because it will naturally be determined by the way you do your code.  As a rule of thumb you should avoid using ‘global’ variables.  Unless you are very careful about their use, they can make your code very hard to understand and maintain.  If you need to make reference to a variable from within a function, then pass the variable in as a parameter to the function.

Really GUI (more on Buttons, images and handlers)

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 few tutorials we have been looking at various parts of what make up a graphical user interface.   We’ve created labels and buttons and have attached images to a label (and a button if you did the homework).  We have also associated an action with pressing a button.   In this tutorial we’re going to put these together to make a simple image viewer application.  As with the previous tutorials, this will only work with images which are saved as a “gif” format.  Also, a word of warning – you need to do pretty much all of the code before any of it will work.

Here are some images for you to use.  As with the earlier tutorial, you can either use ‘save as’ to download a copy, or you can use the Python code in the note below to download them (if you use Python you should be in your python4kids directory when you do the download otherwise the files will get saved somewhere else).  A gold star if you use Python for the download.

Planning

When writing a program it is a good idea to plan out what tasks the program is intended to do first.  Then, when you write the program the plan can guide you from one component to another.  Also, you can write a plan as a comment at the start of your program and it can serve as a docstring – at least until you’ve gone back and written a proper docstring for your code.

Exercise: Use your text editor to make a list of the things that would need to happen in order for a slide show program to run.   Save the list to a file called ‘p4kSlideshow.py’ in your python4kids directory.   Make your edits to this file.  Enclose the list in triple quotes ->

'''
item 1  (and explanation)

item 2 etc

...

'''

Solution:

The things that I think a slideshow should do are:

  • make a list of all the images in the directory
  • get the first image and display it
  • on user input, display the next image (in this case, user input will be a button click)
  • when all images have been displayed exit.

My list of things will likely be different from your list of things.  For example, you might want the images to be displayed in a random order, or when you get to the end of the images you might want to start again from the beginning, or you might want the user to choose the image to show from a list.  There are many ways of approaching it.  I’m showing you something simple, but if you’ve got other things on your list, extend the work we do here and implement them in your own program.

Here’s my note:

'''
 p4kSlideshow
 When run in a directory this program:
 * searches the directory for files with a '.gif' extension
 * displays the first image it finds
 * when a user clicks, it displays the next image
 * if user clicks last image, the program exits.
'''

The benefit of doing this is it tells us what to do (and, hopefully, also in what order – in our case we are dealing with a handler so it’s a little mixed up). It’s our map to completing the program.  Let’s do the first one.

Coding

We are going to use a function from the os module called os.listdir (we have used this before) and another called os.path.splitext.  Splitext splits a file into its base file name and its extension so thisfile.txt would become ['thisfile','.txt'].  We do this to identify which files are gif files.  We could have also used the slicing operator [-3:] (reminder – see here):

import os

IMAGE_EXTENSIONS=['.gif']
listOfFiles = os.listdir('.')
# list dir doesn't necessarily give alphabetical order so we sort
listOfFiles.sort()
listOfImages = []
for f in listOfFiles:
  if os.path.splitext(f)[-1] in IMAGE_EXTENSIONS:
    listOfImages.append(f)

Note that there’s a ‘[-1]‘ after splitext().  This is because splitext returns  a list, with the last entry in the list ([-1] means last in the list) being the extension.  This code stores a list of images in the list named listOfImages.

Now we do the GUI bit.  We need to add the Tkinter import at the start of the file:

import os
from Tkinter import *

We also need to keep track of how many images there are, and which image it is that we are currently up to:

indexes = {}
'''
indexes is a dictionary containing some values
that we need to keep track of.
I am using a dictionary rather than two variables (say totalImages and currentImage)
so that I don't have to explain global and local variables to you yet (maybe next tute?)
'''
indexes['totalImages'] = len(listOfImages)
if indexes['totalImages'] <1:
 sys.exit()

After that we set up a button:

# set up the GUI

displayButton = Button(None,text= "If you can see this text, there's something wrong")
# we shouldn't ever see the text
# we could leave the text= parameter out

displayButton.pack()

Now we create a function to handle clicks.  Later we will need to point the button’s ‘command’ key at this function.

def nextImage():
  indexes['currentImage'] += 1 
  # += 1 says "add one to the current value of this variable"
  if indexes['currentImage'] == indexes['totalImages']:
    # if past last image, then exit
    # as the list starts at index 0 and runs to the number of entries -1
    # it will reach the last one on total entries -1, if it's equal to totalImages
    # then when we added one, we went past the last one, so exit instead of displaying.
    sys.exit()
  else:
    imageIndex = indexes['currentImage']

  imageFile = listOfImages[imageIndex]
  print "imageFile = %s"%imageFile
  # this is just for debugging, you can remove the print statement if you want
  imageObject = PhotoImage(file=imageFile)
  displayButton['image']=imageObject
  displayButton.image=imageObject # NOTE: WON'T WORK WITHOUT THIS BIT OF SORCERY!
  displayButton.pack()

Note the note!  This assignment really is sorcery.  If you remove it your program (probably) won’t work!  Moreover, if I didn’t tell you there’s no way you’d work it out for yourself.  What this line does is it forces Python to keep a reference to the imageObject.  Because of the way Tkinter does the assignment displayButton['image'], Python does not keep a reference to the object imageObject.  Because it has not kept a reference, Python thinks the object is not being used (even though it is). Python therefore throws the imageObject out in order to save memory.  This process is called “garbage collection” and is happening all the time in Python.  When we do the assignment displayButton.image=imageObject we force Python to remember the object and therefore not to throw it out with the rest of the garbage.

Exercise: once you’ve finished the tute comment out the line displayButton.image=imageObject run it again and see what happens.

We are just about ready now.   For the function nextImage() to work the entry indexes['currentImage'] needs to be initialised. We also need to display the first image and hook up the function to the button click.  These are all pretty easy:

# initialise the index we will use to display the images.
indexes['currentImage'] = -1

We use -1 here so that on the first call to nextImage 1 will be added to it, giving 0 (-1+1=0) and 0 is the first entry of the list of file names we just created.

Display the first image:

nextImage()

Hook up the nextImage function to be run when the button is clicked:

displayButton['command']=nextImage

Now, we need to start the program waiting for mouse clicks.  This has not been something we’ve done in the past few tutes because we were using an interactive Python session.  However, now we’re going to need it.  We do this by calling the ‘mainloop()’ method:

displayButton.mainloop()

Ta da! We’re done (exercise: check that we’ve implemented everything in our original list).  Run this program from your command line like this:

> python p4kSlideshow.py

and you should have a simple slideshow.  You must make sure you’ve got the gif files saved in the same directory though!

When the program hits the mainloop() function it sort of slips into a Tkinter Netherworld, in which you don’t really see what it’s doing.  It is only when Tkinter notices a mouse click on the button that control comes back to your program (executing the handler function nextImage()).  Even then, when nextImage() finishes running, program execution passes back into Tkinter and you can’t see what’s happening there.

There are a lot of limitations with this program.  Most obviously it only works with .gif files! Argh!  However, it has other problems.  First, you can’t go backwards – it’s one way from start to finish, and then the program ends.  You might want to start the slideshow from the start again.  Second,  it is quite fragile.  If one of the files is deleted, renamed or moved half way through your slide show (ie after running os.listdir() but before displaying the file) then you’re going to have a problem.  The program doesn’t check that the filename remains valid.  The program is unhelpful if there are no gif files in the directory (simply exiting).  There are many ways the program could be improved.  If you are game, have a go!

Exercise: change the program so that, once it displays all of the images it begins again with the first one.

Hint: at the end of the function nextImage() test to see if you’re at the end of the images and, if so, assign a certain value to indexes['currentImage'].

This was quite a complex program because we had to knit three components together.  Moreover, handlers are pretty difficult conceptually.  If you can follow what’s happening and get it running, give yourself a gold star.

Notes:

Downloading the images:

>>> imageLinks = ["http://python4kids.files.wordpress.com/2011/08/p4kbuttonimage1.gif", "http://python4kids.files.wordpress.com/2011/08/p4kbuttonimage2.gif", "http://python4kids.files.wordpress.com/2011/08/p4kbuttonimage3.gif", "http://python4kids.files.wordpress.com/2011/08/p4kbuttonimage4.gif"]
>>> import urllib
>>> for i in imageLinks:
...     filename =  i[-19:]   # [-19:] was trial and error and based on a set length of the filename
...     urllib.urlretrieve(i,filename)

The complete code:

# -*- coding: utf-8 -*-
'''
p4kSlideshow
When run in a directory this program:
* searches the directory for files with a '.gif' extension
* displays the first image it finds
* when a user clicks, it displays the next image
* if user clicks last image, the program exits.
'''

import os
import sys
from Tkinter import *

IMAGE_EXTENSIONS=['.gif']

listOfFiles = os.listdir('.')
# list dir doesn't necessarily give alphabetical order so we sort
listOfFiles.sort()
listOfImages = []
for f in listOfFiles:
  if os.path.splitext(f)[-1] in IMAGE_EXTENSIONS:
    listOfImages.append(f)

# some print statements I used for debugging the code:
#print listOfFiles
#print listOfImages

# if there are no image files, then stop!

indexes = {}
'''
indexes is a dictionary containing some values
that we need to keep track of.
I am using a dictionary rather than two variables (say totalImages and currentImage)
so that I don't have to explain global and local variables to you yet (maybe next tute?)
'''

indexes['totalImages'] = len(listOfImages)
if indexes['totalImages'] <1:
  sys.exit()

# set up the GUI

displayButton = Button(None,text= "If you can see this text, there's something wrong")
# we shouldn't ever see the text
# we could leave the text= parameter out

displayButton.pack()

# create a function to handle clicks

def nextImage():
  indexes['currentImage'] += 1
  # += 1 says add one to this variable
  if indexes['currentImage'] == indexes['totalImages']:
    # if past last image, then exit
    # as the list starts at index 0 and runs to the number of entries -1
    # it will reach the last one on total entries -1, if it's equal to totalImages
    # then when we added one, we went past the last one, so exit instead of displaying.
    sys.exit()
  else:
    imageIndex = indexes['currentImage']

  imageFile = listOfImages[imageIndex]
  print "imageFile = %s"%imageFile
  # this is just for debugging, you can remove the print statement if you want
  imageObject = PhotoImage(file=imageFile)
  displayButton['image']=imageObject
  displayButton.image=imageObject # NOTE: WON'T WORK WITHOUT THIS BIT OF SORCERY!
  displayButton.pack()

# initialise the index we will use to display the images.
indexes['currentImage'] = -1
'''
-1 is a bit of a magic number
We use -1 because the nextImage function below adds one each time it is called
so the first time it is called it will display the image at location 0 (-1+1 =0)
the next time it is called it will display the image at location 1 and so on
'''

# display the first image
nextImage()

# set the button to run the nextImage function when clicked
displayButton['command']=nextImage

# Now start the program waiting for mouse clicks
# when you click the button it will run the nextImage function
displayButton.mainloop()

Using Images in the GUI

Gladys leaps over to the tape deck, presses levers and switches. Sound of tape reversing. There is a hum and lights flash on and off. A blurred image of a lady in the street comes up on one of the monitors.

In this tutorial we look at using images in a GUI environment.  One of the possible attributes which can be assigned to a Label or Button widget is an image.   You will surely have seen many buttons with images on them.   Every time you see a toolbar in a program, each of the images on it is itself on a button widget.  In order to do this tutorial though there are a couple of issues:

  • first, you need to have images in the Python4kids directory we set up several tutorials ago.  You also need to start Python from within that directory.
  • second, unless you install something called the Python Imaging Library (PIL), Python can only handle a few limited types of image file.  One of the file types which will work is .gif, which we will be using here. This tutorial will not work with jpeg files.
  • third, you need to have the gif files we’re going to use in your python for kids directory.

If you have some gifs that you’d prefer to use, by all means copy them into your python for Kids directory.  However, if you can’t, here is an outrageously boring gif  (it is  just a screencapture from the previous tutorial) for you to download and save:

Download and save this gif image by right-clicking the images in the previous tutorial and selecting “Save Link As” (you may also be able to drag and drop images from the web page into your folder) [* or do it in Python (below)].  Remember to save it to the Python for kids directory that you set up in this tutorial.  You don’t need to change the file name.   If you have your own image file(s) you’d like to use, then, by all means put them in your Python4kids directory instead but make sure it is a ‘.gif’ file.  You can convert images from .jpg to gif using third party programs such as GIMP.

As before we import from Tkinter:

>>> from Tkinter import *
>>> labelWidget = Label(None)
>>> # Notice we haven't set the text?

Next, we make a PhotoImage object from the file (taking note of the file’s name as we downloaded it)

>>> imgFile = 'p4ktkinter110726b.gif'
>>> imageObject = PhotoImage(file = imgFile)

Watch the “file = ” syntax here.  We haven’t attached the file to the labelWidget yet.  We’ll do that very soon.  However, now if we:

>>> labelWidget.pack()

You’ll see that we get a really small labelWidget with nothing in it. You might need to resize it to see much of it at all.

So let’s put the image we have into the labelWidget:

>>> labelWidget['image']=imageObject

Note here that we are using the dictionary associated with labelWidget, you should be able to tell that because of the square brackets and the key which is inside single quotes.  At this point the object should automagically appear in the widget – if you haven’t resized the window yet you may need to in order to see the image:

It looks like there is a window inside the window, but there isn’t.  It’s just an image (try opening up the gif file and changing it with a paint package if you have one and then redoing the tutorial).  Clicking the buttons won’t have any effect.

Homework:

Display an image on a Button

* Extension: Downloading the file using Python:

>>> url = "http://python4kids.files.wordpress.com/2011/08/p4ktkinter110726b.gif"
>>> fileName = url[-21:]
>>> fileName
'p4ktkinter110726b.gif'
>>> import urllib
>>> urllib.urlretrieve(url,fileName)
('p4ktkinter110726b.gif', <httplib.HTTPMessage instance at 0x82695ac>)

This relies only on knowing what the url of the image file is.  We’re using the urlretrieve function of the urllib module.  It will work for urls which are not images as well.  In order to download a file it needs to know the url of the file (ie what you would type into a web browser to surf to the file) and a file name to save the to on your computer.

A word of warning though – sometimes a network is set up so you that this will not work.  If you have trouble, better to just save as with your browser.

Follow

Get every new post delivered to your Inbox.

Join 72 other followers