More Python

Tuples

Similar to lists, but immutable.

>>> t = ('a', 'b', 'c')
>>> for elem in t:
...     print elem
a
b
c
>>> print t[1]
b
>>> t.sort()
...
AttributeError: 'tuple' object has no attribute 'sort'

Please ignore what I just said.

Despite the surface similarities, tuples have a very different purpose from lists.

A list is for a variable-size collection of similar things.

A tuple is generally for describing multiple aspects of a single thing, where each spot in the tuple has an (implicit) meaning. For instance, a tuple about me: ('Carl', 'Meyer', 29, 74, 165).

But if we've got a group of people, use a list (of tuples!):

[('Carl', 'Meyer', 29, 74, 165),
 ('Fred', 'Flintstone', 43, 65, 257)]

Summary

Tuples are fine for simple data structures, where you can easily keep in your head what field means what. Later we'll cover classes, a more explicit way to define compound data structures.

String formatting

Last week we saw simple string concatenation: print 'Your name is ' + first_name + ' ' + last_name. This breaks if we try to concatenate something that isn't a string:

>>> age = 29
>>> print 'You are ' + age + ' years old.
...
TypeError: cannot concatenate 'str' and 'int' objects

Instead...

In most cases, you're better off with string formatting:

people = [('Carl', 'Meyer', 29, 74, 165),
          ('Fred', 'Flintstone', 43, 65, 257)]
for person in people:
    fn, ln, age, height, weight = person
    print '%s %s is %s years old, %s inches tall, and weighs %s.' % (fn, ln, age, height, weight)

Or...

people = [('Carl', 'Meyer', 29, 74, 165),
          ('Fred', 'Flintstone', 43, 65, 257)]
for person in people:
    print '%s %s is %s years old, %s inches tall, and weighs %s.' % person

Splitting strings

Say we're working with file paths, so we have a variable path = '/Volumes/HOMES/Classes/COMM385'. We want to know what the last bit of that path is. So we split the string:

>>> bits = path.split('/')
>>> bits
['', 'Volumes', 'HOMES', 'Classes', 'COMM385']

>>> bits[-1]
'COMM385'

Joining lists

Now we'll change the last bit of the path, and then put it back together into a single string:

>>> bits[-1] = 'CS101'
>>> bits
['', 'Volumes', 'HOMES', 'Classes', 'CS101']

>>> '/'.join(bits)
'/Volumes/HOMES/Classes/CS101'

More on Functions

Functions can have multiple arguments (of course), and some of them can be optional:

def first(a_list, num=1):
    """
    Return the first X elements in a list (one element by default).

    """
    return a_list[:num]
So we can call this function with either one or two arguments:
>>> my_list = [0, 1, 2, 3]
>>> first(my_list)
[0]
>>> first(my_list, 3)
[0, 1, 2]

Using named arguments

If a function has multiple optional arguments, we can specify exactly which ones we want to override when we call it:

def html_tag(contents, tag='p', attributes=None):
    "Create an HTML tag with contents."
    if attributes is None:
        attrs_string = ''
    else:
        attrs_string = ' '.join(['%s="%s"' % (k,v)
                                 for k,v 
                                 in attributes.items()])
    return "<%s%s>%s</%s>" % (tag, attrs_string, contents, tag)

>>> html_tag('Some stuff', attributes={'id': 'stuff'})
'<p id="stuff">Some stuff</p>'

I got better

We can allow a function to take an arbitrary number of "keyword arguments":

def html_tag(contents, tag='p', **attributes):
    "Create an HTML tag with contents."
    print attributes
    if not attributes:
        attrs_string = ''
    else:
        attrs_string = ' '.join(['%s="%s"' % (k,v)
                                 for k,v 
                                 in attributes.items()])
    return "<%s%s>%s</%s>" % (tag, attrs_string, contents, tag)

>>> html_tag('Some stuff', tag='div', id='stuff', class='aside')
{'id': 'stuff', 'class': 'aside'}
'<div id="stuff" class="aside">Some stuff</div>'

Help! I'm being oppressed!

With a single asterisk, your function can collect an arbitrary number of non-keyword arguments:

def sum(*args):
    "Return sum of all arguments."
    total = 0
    for arg in args:
        total += arg
    return total

>>> sum(1, 2, 3)
6
>>> sum(2, 4, 6, 8, 10)
30

Come see the violence inherent in the system

And, of course, you can have both *args and **kwargs in the same function:

def print_args(*args, **kwargs):
    "Print out all arguments."
    print args
    print kwargs

>>> print_args(1, 2, 3, foo="bar", baz="quux")
[1, 2, 3]
{'foo': 'bar', 'baz': 'quux'}

One more functional brain-bender

In Python, functions are first-class values. That means we can refer to them, pass them around, assign them to variables, just like we can with numbers, strings, etc. We can even write functions that take other functions as arguments:

def apply_twice(func, argument):
    """
    Applies a function to one argument, and then applies the 
    same function again to the result.

    """
    return func(func(argument))

>>> def double(x): return x*2
...
>>> double(3)
6
>>> apply_twice(double, 5)

I left out the result of that last function call - what will it be?

Reading

Chapter 4 in Dive Into Python

Homework

...and test it

For each of these functions, demonstrate that they work by writing test code that uses them. Use the if __name__ == '__main__': trick discussed in Dive Into Python section 2.6 so that your test code runs if you run python my_file.py, but does not run if you import your module.