Programming 1, 2021-2
Week 10: Notes

functions as values

In Python, functions are first-class values. That means that we can work with functions just like with other values such as integers and strings: we can refer to functions with variables, pass them as arguments, return them from other functions, and so on.

Here is a Python function that adds the numbers from 1 to 1,000,000:

def bigSum():
    sum = 0
    for i in range(1, 1_000_001):
        sum += i
    return sum

We can put this function into a variable f:

>>> f = bigSum

And now we can call f just like the original function bigSum:

>>> f()
500000500000

Let's write a function time_it that takes a function as an argument:

def time_it(f):
    start = time.time()
    x = f()
    end = time.time()
    print(f'function ran in {end - start:.2f} seconds')
    return x

Given any function f, time_it runs f and measures the time that elapses while f is running. It prints this elapsed time, and then returns whatever f returned:

>>> time_it(big_sum)
function ran in 0.04 seconds
500000500000

This is a first example illustrating that it can be useful to pass functions to functions. As we will see, there are many other reasons why we might want to do this.

Here's a function my_map that takes arguments f (a function) and list (a list). The function applies f to each element of the list, and collects the results into a list that it returns:

def my_map(f, list):
    a = []
    for x in list:
        a.append(f(x))
    return a

Here is how we might use my_map:

def twice(x):
    return x * 2 

>>> my_map(twice, [10, 20, 30])
[20, 40, 60]

In the returned list, every value in the input list has been doubled.

In fact this function is so useful that it exists in Python's standard library, where it is just called 'map'. However, the library version returns an iterable, not a list:

>>> map(twice, [10, 20, 30])
<map object at 0x7f44fb0ebe80>

We can easily convert the iterable to a list if we like:

>>> list(map(twice, [10, 20, 30]))
[20, 40, 60]

As another example, here is a function max_by that finds the maximum value in an input sequence, applying a function f to each element to yield a comparison key:

def max_by(seq, f):
    max_elem = None
    max_val = None
    for x in seq:
        v = f(x)
        if max_elem == None or v > max_val:
            max_elem = x
            max_val = v
    return max_elem

We can use max_by to find the longest list in a list of lists:

>>> max_by([[1, 7], [3, 4, 5], [2]], len)
[3, 4, 5]

Or we can use it to find the list whose last element is greatest:

def last(s):
    return s[-1]

>>> max_by([[1, 7], [3, 4, 5], [2]], last)
[1, 7]

This capability is so useful that it's also built into the standard library. The standard function max can take a keyword argument key holding a function that works exactly like the second argument to max_by:

>>> max([[1, 7], [3, 4, 5], [2]], key = len)
[3, 4, 5]

The built-in function sorted and the sort() method take a similar key argument, so that you can sort by any attribute you like. For example:

>>> l = [[2, 7], [1, 3, 5, 2], [3, 10, 6], [8]]
>>> l.sort(key = len)
>>> l
[[8], [2, 7], [3, 10, 6], [1, 3, 5, 2]]

methods as values

We have just seen that a Python variable may refer to a function. It may also refer to a method of a particular object.

For example, consider this class:

class Counter:
    def __init__(self):
        self.count = 0
    
    def inc(self):
        self.count += 1

Let's create a couple of instances of Counter, and a variable 'f' that refers to the 'inc' method of one of those instances:

>>> c = Counter()
>>> c.inc()
>>> c.inc()
>>> c.count
2
>>> d = Counter()
>>> d.count
0
>>> f = c.inc

When we call f(), it will increment the count in the object c:

>>> f()
>>> f()
>>> c.count
4

The value in 'd' remains unchanged, since f refers to the inc() method of c, not d:

>>> d.count
0

lambda expressions

Let's return to the previous example where we were given a list of lists, and found the list whose last element is greatest:

def last(s):
    return s[-1]

>>> max_by([[1, 7], [3, 4, 5], [2]], last)
[1, 7]

It's a bit of a nuisance to have to define a separate function last here. Instead, we can use a lambda expression:

>>> max_by([[1, 7], [3, 4, 5], [2]], lambda l: l[-1])
[1, 7]

A lambda expression creates a function "on the fly", without giving it a name. In other words, a lambda expression creates an anonymous function.

A function created by a lambda expression is no different from any other function: we can call it, pass it as an argument, and so forth. Even though the function is initially anonymous, we can certainly put it into a variable:

>>> abc = lambda x, y: 2 * x + y
>>> abc(10, 3)
23

The assignment to abc above is basically equivalent to

def abc(x, y):
    return 2 * x + y

which is how we would more typically define this function.

drawing graphics

We may wish to display graphics in many programs. A typical notebook or monitor has a graphical display with a resolution of 1920 x 1080 pixels. Typically each pixel's color is a combination of red, green, and blue components, each of which is an 8-bit value.

Generally we will use a graphics library (also called a toolkit) to perform graphical output. Such libraries will allow us to draw low-level shapes such as lines, rectangles, and circles, and will usually provide higher-level widgets such as menus, buttons, toolbars that we can use to build more complex graphical interfaces. The term GUI (= graphical user interface) generally refers to a user interface of this sort.

Some popular cross-platform graphics toolkits include GTK and Qt. These are large and complex, and are accessible from many languages including Python. Most desktop applications that come with Ubuntu and other Linux distributions use either GTK or Qt.

Additionally, there exist various Python-specific libraries including Tkinter and Pygame. In this class we will study Tkinter. It is part of the standard library in every Python installation, and is arguably the standard graphical user interface library for Python.

Tkinter is a large library, and we will only study a small part of it in this course. I've created a Tkinter Quick Reference page that describes the Tkinter classes and methods that we will use.

For more information, the site TkDocs has a Tkinter tutorial and points to the Tkinter reference pages. The book Modern Tkinter is inexpensive and is also helpful.

drawing on a canvas

Here is a first Tkinter program that creates a canvas and draws a red circle on it:

from tkinter import *

root = Tk()         # create a top-level window for the application
root.title('circle')

canvas = Canvas(root, width = 600, height = 400)
canvas.grid()

canvas.create_oval(200, 100, 400, 300, outline = 'red', width = 5)

root.mainloop()     # run the main loop

Let's look at the program line by line. First, you will probably want to write 'from tkinter import *' at the top of any program that uses Tkinter, so that you won't have to write the prefix "tkinter." everywhere in your code.

The Tk() constructor creates a top-level window for your application. You can call the .title() method on this window to set its title.

Next, we create a Canvas, which is a widget that allows you to draw low-level graphical shapes such as lines and circles. As we create the canvas, we specify values for two configuration options, namely 'width' and 'height' which determine the canvas's size. So that the canvas will be visible, we must call canvas.grid() to register it with the grid layout manager, which determines the positions and sizes of widgets in a window.

A canvas may contain various canvas items such as lines, rectangles, or images. The call create_oval() creates an oval item, which is an ellipse (of which a circle is a special case). As our program creates an oval item, it specifies two item configuration options, namely 'outline', which is the color of the outline around the oval shape, and 'width', which is the width of the outline in pixels.

Finally, we call the mainloop() method which tells Tkinter to run the main loop for our program. Typically any graphics library has such a main loop, which waits for events to occur and dispatches them to event handler functions. In this first program we don't handle any events; we simply display graphics and wait for the user to close the window. Nevertheless we must still call mainloop().

reacting to events

Let's modify our program so that the circle's color will change when the user clicks the mouse. Here's the updated program:

from tkinter import *

root = Tk()         # create a top-level window for the application
root.title('circle')

canvas = Canvas(root, width = 600, height = 400)
canvas.grid()

oval_id = canvas.create_oval(200, 100, 400, 300, outline = 'red', width = 5)

def on_click(event):
    # change the oval color to green
    canvas.itemconfigure(oval_id, outline = 'green')

canvas.bind('<Button>', on_click)

root.mainloop()     # run the main loop

In this program, on_click() is an event handler function that we want to run whenever the user clicks the mouse. We call the .bind() method to register this event handler to run in response to the '<Button>' event. (You can read about other event types in our quick reference guide.)

Every event handler function receives an argument 'event' that points to an Event object that contains information about the event, such as the x/y coordinates of the position where the user clicked the mouse. In this program we ignore the Event object, since we don't care about where the user clicked. Instead, we just call the itemconfigure() method of the Canvas object, which we can use to update the configuration options of a canvas item. Specifically, the call to create_oval() returned an integer id, and we can pass this id to itemconfigure() to change the oval outline color to green.