Homework review

"Instruments of Torture"

red pen and marked-up homework

http://www.flickr.com/photos/alstonfamily/2237347597

Exercise 1: return largest element in list

One try:

def maximum(numbers):
    """
    Return the largest element in a list.

    """
    high = 0
    for x in numbers:
        if x > high:
            high = x
    return high

But this doesn't work in every case! How is it broken? How can it be fixed?

There are numbers smaller than zero?

>>> from utils import maximum
>>> maximum([-2, -4, -1])
0

A better version

def maximum(numbers):
    """
    Return the largest element in a list.

    """
    high = numbers[0]
    for x in numbers:
        if x > high:
            high = x
    return high

Now we can handle an all-negative list, too. But we start with the first element of the list, and then the first time through the loop we compare that to itself. Is that really necessary?

Too clever?

def maximum(numbers):
    """
    Return the largest element in a list.

    """
    high = numbers[0]
    for x in numbers[1:]:
        if x > high:
            high = x
    return high

Clever. But: we are now copying the entire list minus one element, which could be very expensive if we are passed a large list. Probably better to do one extra comparison than to copy the entire list. But I digress...

And now for something completely different

def maximum(numbers):
    """
    Return the largest element in a list.

    """
    numbers.sort()
    return numbers[-1]

Lists have a built-in .sort() method! But...

Danger!

Function arguments in Python are passed by reference, not by value. If an argument is mutable (strings and numbers are not, lists and dictionaries are), if you change it inside the function the change will appear outside the function, too:

>>> from utils import maximum
>>> my_list = [2, 1, 7, 4, 3]
>>> maximum(my_list)
7
>>> my_list
[1, 2, 3, 4, 7]

This might surprise someone calling your function!

A solution: sorted()

def maximum(numbers):
    """
    Return the largest element in a list.

    """
    return sorted(numbers)[-1]

sorted() returns a sorted version of the list, it doesn't modify the list you pass to it.

I told them we've already got one

All that work... and it's actually already built-in to Python:

>>> my_list = [2, 1, 7, 4, 3]
>>> max(my_list)
7

Exercise 2: return name of oldest in names->ages dict

def oldest(people): 
    """ 
    Expects a dictionary of people's names -> ages. 
    Returns the name of the oldest person.

    """
    high = None
    oldest = None
    for name in people:
        age = people[name]
        if high is None or age > high:
            high = age
            oldest = name
    return oldest

This is subtly broken, too; or at least under-specified, because the assignment itself was not clearly specified. How?

Multiple people can have the same age?

>>> from utils import oldest
>>> people = {'Jeffrey': 23, 'Aloysius': 37, 'Selena': 37}
>>> oldest(people)
Aloysius

Building up a list

def oldest(people): 
    """ 
    Expects a dictionary of people's names -> ages. 
    Returns the name of the oldest person.

    """
    high = None
    oldests = []
    for name in people:
        age = people[name]
        if high is None or age > high:
            high = age
            oldests = [name]
        elif age == high:
            oldests.append(name)
    return oldests

Exercise 3: square all the numbers in a list

def squared(numbers):
    """
    Expects a list of numbers.
    Returns a corresponding list with each element squared.

    """
    return [x**2 for x in numbers]

Pretty simple.

Common problems

car missing a wheel

http://www.flickr.com/photos/ctorok/358096814/

print vs return

>>> def times2(x): print x*2
>>> a = times2(7)
14
>>> print a
None

>>> def times2(x): return x*2
>>> a = times2(7)
>>> print a
14

In web apps, we never use print (except maybe debugging). Nobody's looking at the server screen. A function that generates some value should return that value so the rest of your program can use it.

Iterating vs indexing

for elem in my_list:
    ... do something with elem ...

for i in range(len(mylist)):
    elem = mylist[i]
    ... do something with elem ...

In Python, you very rarely want the latter. Why? The former is cleaner, easier to read, and works with a wider variety of "iterable" objects. In those rare cases when you really need the counter variable, do this:

for i, elem in enumerate(mylist):
    ... do something with i and elem ...

Overriding builtin names

Python lets you do this. But it's a bad idea, because it makes things confusing. "Is 'list' here the built-in type 'list', or some other variable 'list'"?

>>> print list
<type 'list'>
>>> def some_func(list):
...     print list
>>> some_func('blah')
'blah'

Some names of builtin types: list, dict, str, bool. Also None. Also all the functions you see when you type dir(__builtins__).

Code reuse: the holy grail

Don't hesitate to reuse your own code! That's what functions are for; to allow you to set apart a logical chunk of code and reuse it. Don't reimplement something if you've already written a function to do it.

Or if someone else has.

(note to self: stop assigning homework exercises to reimplement builtin functions, then.)

Coding style

Function and variable names should be lowercase, with underscores as needed for readability. Module (file) names lowercase, with underscores used sparingly if at all. my_list, utils.py &c.

Avoid variables named "l", "O", or "I", because you're just asking for trouble.

For all the gory details, read PEP 8.

Why bother? Readability and maintainability count (code is read far more often than it is written). These are the conventions other Python programmers will expect.