Computer Science Canada

[Python-tut] Control Flow: if/else, loops, exceptions

Author:  wtd [ Thu Apr 07, 2005 8:11 pm ]
Post subject:  [Python-tut] Control Flow: if/else, loops, exceptions

In previous tutorials on classes and functions, we've seen glimpses of Python's control structures. Here I'll attempt to provide a bit closer view.

If/else

In keeping with the Python language's (somewhat rigid) belief that there should be one way to do things, there is a single form of if statement. As with everything else, it uses the colon and indentation scheme.

code:
if some_condition:
   do_this
elif some_other_condition:
   do_this_instead
else:
   do_this_if_nothing_else


That's the basic form. Note that instead of "else if", "elseif", or "elsif," Python uses the even shorter "elif." This isn't a particularly big hurdle to overcome in learning Python, butit is just a little bit different from most everyting else.

The interesting thing to consider with the if statement is what kind of boolean statements you can write. Python considers zero (either as an int or float), and empty strings, lists and dictionaries false. Any function that returns these values or the explicit value False can be considered false. Everything else is true.

For comparisons, we can use the ==, !=, <, >, <=, and >= operators. Additionally, we can use "is" for == and "not" to negate any boolean value. "is not" may be used, but may not always work the way one expects. For that matter, "is" may not alway behave as expected. To chain boolean expressions, we can use "and" and "or."

Python has no "switch" statement, so we'll move right on to loops.

Loops

The most basic loop form in Python is the while loop. Let's consider a while loop that counts from zero to ten.

code:
>>> counter = 0
>>> while counter < 11:
...    print counter
...    counter += 1
...
0
1
2
3
4
5
6
7
8
9
10
>>>


Again, pretty self-explanatory, which is not of the nice things about Python.

The second, and only other form of loop is Python's for-each loop. I hesitate to simply call it "for" since that name is used in many C-like languages for something quite different. Python's for-each loop iterates over a collection, rather than going from one initial state to a final state with some user-specified updater.

Values that can be iterated over include strings, lists, and dictionaries. In the case of the last, by default only the keys are iterated over.

code:
>>> for x in "foo":
...    print x
...
f
o
o
>>> for x in [1,2,3]:
...    print x
...
1
2
3
>>> for x in {"foo": "bar", "hello": "world"}:
...    print x
...
foo
hello
>>>


One might want each item in the collection numbered.

code:
>>> counter = 0
>>> for x in "hello":
...    print counter, x
...    counter += 1
...
0 h
1 e
2 l
3 l
4 o
>>>


But this is rather more work than we have to do.

code:
>>> for i, x in enumerate("hello"):
...    print i, x
...
0 h
1 e
2 l
3 l
4 o
>>>


As we can see, it's quite possible and easy to unpack multiple values from a collection, and the enumerate generator intersperses numbers with the original data. More on generators in a bit.

I mentioned that by default, we can only iterate over the keys in a dictionary. But what if we want to iterate over both the keys and values? Well, we can use the iteritems generator method.

code:
>>> d = {"foo": "bar", "hello": "world"}
>>> for key, value in d.iteritems():
...    print key, value
...
foo bar
hello world
>>>


Using enumerate with dictionaries is a bit more complicated. Enumerate returns two values: the index and the item from the collection. Yet here the item itself has two values. So we need to use pattern matching (somewhat like that from ML and Haskell but less powerful).

code:
>>> d = {"foo": "bar", "hello": "world"}
>>> for i, (k, v) in enumerate(d.iteritems()):
...    print i, k, v
...
0 foo bar
1 hello world
>>>


Exceptions

Before talking about generators, let's discuss exception handling, since the two are related.

An exception is a way of indicating that something has gone wrong, or at least unexpected. They flag "exceptional" behavior.

Let's take a look at handling an IOError. This one will be caused by trying to open and read a file which doesn't exist. We'll handle it by simply doing nothing, demonstrating use of the "pass" keyword.

code:
>>> try:
...    handle = file("foo.bar")
... except IOError:
...    pass
...
>>>


Of course, we can use more than one except clause for different types of errors, and an except clause with no specific type of exception named will catch any unhandled exceptions.

An optional else clause handles the event that no exceptions are encountered. We could use this for closing the file handle we opened.

code:
>>> try:
...    handle = file("foo.bar")
... except IOError:
...    pass
... else:
...    handle.close()
...
>>>


One other interesting bit of trivia is that we can handle an exception, but then reraise it, so that calling code still has to deal with it. Let's consider writing a error message to stderr.

code:
>>> from sys import stderr
>>> try:
...    handle = file("foo.bar", "r")
... except IOError:
...    stderr.write("An error occurred trying to open foo.bar\n")
...    raise
... else:
...    handle.close()
...
An error occurred trying to open foo.bar
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
IOError: [Errno 2] No such file or directory: 'foo.bar'
>>>


We can of course also raise exceptions ourselves. Let's define a function which takes one argument, and then we'll check to make sure that argument is an integer.

code:
>>> def foo(n):
...    if not isinstance(n, int):
...       raise TypeError
...    return n * 2
...
>>> foo(42)
84
>>> foo("hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 3, in foo
TypeError
>>>


More on exceptions can be found at python.org.

Generators

I mentioned generators earlier. Generators are essentially a means of easing use of custom looping constructs and reducing the need to completely construct lists before any looping happens.

Consider two different means for looping over the key and value pairs in a dictionary.

code:
d = {"foo": "bar", "hello": "world"}
for k, v in d.items():
   print k, v


Here the items methods creates a new list composed of key/value pairs. Instead, we can use the iteritems generator method and more directly loop over them.

code:
d = {"foo": "bar", "hello": "world"}
for k, v in d.iteritems():
   print k, v


In this case iteritems creates a new generator object. The generator object has a next method which returns the item it's pointing to and moves to the next item. Calling next when nothing is left raises the StopIteration exception. Thus the above is essentially equivalent to:

code:
d = {"foo": "bar", "hello": "world"}
gen = d.iteritems()
try:
   while True:
      print gen.next()
except StopIteration:
   pass


So now you can appreciate the for loop a bit more. Let's consider another practical use of a generator. The file class defines a next method, enabling it to be used as a generator.

code:
>>> for line in file("foo.dat"):
...    print line.strip()
...
---
- foo
- bar
>>>


We can create our own generators by using the yield keyword. A simple example:

code:
>>> def foo():
...    for x in range(6):
...       yield x
...
>>> for y in foo():
...    print y
...
0
1
2
3
4
5
>>>


Back to looping for a moment

Let's say we want to get a list and double each element, then put all of those into a new list.

code:
>>> old = range(10)
>>> new = []
>>> for x in old:
...    new.append(x * 2)
...
>>> new
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>>


Instead we can use a list comprehension.

code:
>>> new = [x * 2 for x in old]
>>> new
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>>


What if we wanted to also filter evens out of the original list.

code:
>>> old = range(10)
>>> new = []
>>> for x in old:
...    if x % 2 == 1:
...       new.append(x * 2)
...
>>> new
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 2, 6, 10, 14, 18]
>>> new = [x * 2 for x in old if x % 2 == 1]
>>> new
[2, 6, 10, 14, 18]
>>>


: