Virtuous Programmer Adventures of an Autodidact

8Nov/100

Python 2/4: Loops, Decisions, and Tests

This is the second article in a series, to read the entire series go here.

Conditionals and Loops

Now that Python's installed and we know how to run a program, the next step is to look more closely at Python's conditional structures. The lens we'll use for this is the fizzbuzz problem.

fizzbuzz

fizzbuzz.py

The fizzbuzz problem is derived from a game in which the players sit in a circle and count, one after the other. If the number is divisible by 3 or 5, they say either 'fizz' or 'buzz' insteady of the number. If the number is divisible by both 3 and 5 they say 'fizzbuzz.' A fairly common test of basic programming skill is to write a program that will iterate through a sequence of numbers assigning them the appropriate value from the fizzbuzz game.

I'll be using two different solutions to the fizzbuzz problem to take a look at how Python handles conditionals and loops. One solution will use a for loop, the other will use list comprehension.

Function Formatting

The first point to examine is function definition. In the previous post I skimmed over how blocks are delimited in Python. Here you can see that python uses the off-side rule to indicate the start and end of a block and new lines to indicate the end of a statement. This is wonderful because it makes for a very clean looking syntax, and it eliminates two entire classes of bugs: the 'you forgot a semicolon here' bug and the 'remember to close your braces' bug. It also creates its own problems.

Python interprets both a tab and a space as a single unit of whitespace. It reads 1 tab's worth of spaces and an actual tab as being two different things. This can lead to a lot of frustration for new programmers because these look exactly the same to someone reading the code. To help avoid this kind of problem it's considered standard in the Python community to set your editor to use only spaces and never insert a tab character. How to go about this varies from editor to editor. In IDLE, Python's IDE, tabs are treated as spaces by default.

Most frequently in the line preceding a new block there is a line such as a function declaration, an if statement, a looping statement, those lines will have colon's at their ends indicating the end of the statement/start of the block.

  1. def isfizz(num):
  2.   """Returns true if the given number is divisble by 3"""
  3.   return num % 3 == 0
  4.  
  5. def isbuzz(num):
  6.   """Returns true if the given number is divisble by 5"""
  7.   return num % 5 == 0
  8.  

The other point of interest in the above code is the first line of each of the functions after the declaration. When you write a python function if you start it with a triple quoted string documention generated by pydoc will automatically be incorporated in the function.

If I Were More Assertive

  1. def fizzbuzzSingle(num):
  2.   """This follows the fairly simple pattern of generating
  3.     a single instance of fizz buzz depending on the number
  4.     value given."""
  5.   assert num > 0, "The given number " + str(num) + " is not a natural number."
  6.  
  7.   if isfizz(num) and isbuzz(num):
  8.     return "fizzbuzz"
  9.   elif isfizz(num):
  10.     return "fizz"
  11.   elif isbuzz(num):
  12.     return "buzz"
  13.   else:
  14.     return str(num)
  15.  

A few interesting pythonisms show up here. On line 5 there's an assertion statement with the format "assert <a boolean statement>, <The string to throw if it fails>". This is just syntactic sugar for:

  1.     if not num > 0:
  2.       raise AssertionException("The given number " + str(num) + "is not a natural number."
  3.  

Lines 7-13 contain the standard setup for Python if/elif/else statements. Although Python usually uses parentheses to delimit arguments in a function, they are unnecessary in python conditionals/loops where the colon indicates the end of the statement. Of course that leads to the question, why they are necessary in function definitions. Anyone who can answer this feel free to let me know.

On line 14, there's an explicit type conversion from an integer to a string. This is necessary in Python because implicit type casting isn't allowed. The basic types all have an equivalent to this converstion, this means the programmer only has to learn one function per type, instead of "intToStr", "floatToStr" etc.

Ranges and Method Calls

  1. def fizzbuzzComprehension(topNum):
  2.   """In this example I've used list comprehension to generate the list of
  3.     fizzbuzz strings, then combined them afterwards."""
  4.   fizzbuzzList = [fizzbuzzSingle(num) for num in range(1, topNum + 1)]
  5.   return " ".join(fizzbuzzList)
  6.  

I've used the range function several times, but haven't gotten around to explaining how it works. It shows a few of the nice features in python. At its base, range generates an ordered list of numbers. What that list is depends on the number of arguments.

  1. range(5) # 1 Argument Generates [1, 2, 3, 4]
  2. range(3, 7) # 2 Arguments Generates [3, 4, 5, 6]
  3. range(0, 30, 5) # 3 Arguments Generates [0, 5, 10, 15, 20, 25]
  4.  

The important idea here is that Python allows variable numbers of arguments in its functions, we'll get to more detail on how this works in a later article.

The last line in fizzbuzzComprehension introduces Python's method call notation. The syntax is fairly common in object oriented languages <object>.<method>(<arguments>). Here the object is a string with one space, the method is "join." Join takes a list of strings and intersperses its object between them.

For Loops, Iterables and Slices

  1. def fizzbuzzFor(topNum):
  2.   """Here I've used the more common for loop to generate the fizzbuzz string,
  3.     It's less concise than the list comprehension version, but is also less
  4.     suprising to those who aren't as familiar with the syntax."""
  5.   fizzbuzzString = ""
  6.   for num in range(1, topNum + 1):
  7.     fizzbuzzString += fizzbuzzSingle(num) + " "
  8.  
  9.   # Return the total string, dropping the last space.
  10.   return fizzbuzzString[:-1]
  11.  

On line 6 we introduce Python's version of the for loop. Unlike most languages I've encountered where the format is "for(<start value>, <end condition>, <value change>)", Python's format is "for <new variable> in <iterable>". An iterable, broadly described, is any value that you could perceive as a list of something. Lists, lines in a file, the output of a generator and a sets are a few examples of iterables.

The last line will look especially unusual to those who are used to conventional array index notation. Python uses a rather power syntax called slice notation to refer to subsets of an iterable value where 0 is the first value. You can refer to sub-lists with the syntax "<a list>[<first value>:<value after the last value>]". If you leave out either the start or end value then Python will take from the beginning or the end respectively. Finally, as can be seen in fizzbuzzFor, you can use negative numbers to refer to start from the end of the iterable rather than the beginning. To make the functionality more clear, read a few examples:

  1. "spam"[0:1] == "s"
  2. "spam"[:3] == "spa"
  3. "spam"[-2:] == "am"
  4. "spam"[:-1] == "spa"
  5.  

Making it Run

  1. if __name__ == "__main__":
  2.   print fizzbuzzComprehension(20)
  3.  

As I mentioned in the last article, Python doesn't have a 'main' function, but there are times when you want to load a module without having it execute all of its code. If you want code that is only executed when the file is executed on its own you check if the the __name__ variable's value is "__main__".

Testing.... Testing

test_fizzbuzz.py

Python comes with its unit test framework built in, among many other libraries. There are a number of others available, but pyunit (called unittest in the python 2.7 release) is more than adequate.

Importing Modules/Libraries

  1. import unittest
  2. from fizzbuzz import *
  3.  

Both of the above commands import the named libraries. The two differ in how much information you need to reference the contents of their modules. The functions on fizzbuzz can be referenced as though they were declared in the same file. To refer to an element in the unittest module you must indicate that you are referring to a member of that module with dot notation like "<module>.<element>". Unfortunately this notation is confusingly similar to the notation used to reference the methods in objects.

The notation "from <module> import <something>" can either be general and import all of the elements in the module as above, or import specific elements as in:

  1. from fizzbuzz import fizzbuzzSingle, fizzbuzzComprehension

Classes

  1. class FizzBuzz(unittest.TestCase):
  2.   def setUp(self):
  3.     """Setup is run every time a given test is executed."""
  4.     self.justBy3 = [num for num in range(1, 101) if isfizz(num) and not isbuzz(num)]
  5.     self.justBy5 = [num for num in range(1, 101) if isbuzz(num) and not isfizz(num)]
  6.     self.by3and5 = [num for num in range(1, 101) if isfizz(num) and isbuzz(num)]
  7.     self.notBy3and5 = [num for num in range(1, 101) if not (isfizz(num) or isbuzz(num))]
  8.  

Class syntax in Python isn't especially suprising, "class <name of class> (<superclasses>)". It's worth noting that Python allows for multiple inheritance and that methods are declared in a similar fashion to functions.

All methods have 'self' as their first argument, this allows you to refer to the object that contains the method without any extra keywords. This can cause some confusion because while all methods have 'self' as their first argument when declared, when the method is called 'self' is left off.

  1.   def testFizzBy3(self):
  2.     """Verifies that if a number is divisble by 3, it returns fizz"""
  3.     for num in self.justBy3:
  4.       result = fizzbuzzSingle(num)
  5.       self.assertEqual(result, "fizz")
  6.  

Pyunit makes use of the exception handling system for its tests. In this case, should the assertion in line 5 fail then the test fails. Also note in line 3, the use of the list generated in the setUp method.

  1.   def testMustBePositive(self):
  2.     """fizzbuzzSingle should throw an error when given a non-positive value"""
  3.     try:
  4.       fizzbuzzSingle(0)
  5.     except AssertionError:
  6.       pass
  7.     else:
  8.       fail("Expected an assertion error, non-positive should fail.")
  9.  

In cases where an error is the expected behavior, using the try/except/else syntax demonstrated that the error was thrown. Here Python is actually using a pun on pass/fail. Fail is a legitimate part of the unittest library. Pass is a python keyword indicating that it should do nothing. It's similar to NOP in assembler code.

  1. if __name__ == "__main__":
  2.   unittest.main()
  3.  

The main() method in unittest uses reflection to extract and run all of the methods in the object that start with "test" and executes them as the unit test suite.

Resources

Python.org
PyUnit / Python's Unittest Docs
Python Cheat Sheet
The first article in this sequence

Posted by Frank Berthold

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.