Virtuous Programmer Adventures of an Autodidact

A Month With Python

What's this all about?
1Nov/103

Python 1/4: Getting Started

Posted by Frank Berthold

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

Why Python is Interesting

Programs must be written for people to read,
and only incidentally for machines to execute.
-Abelson and Sussman

Readable Syntax

When Guido van Rostrum designed Python it's clear that he had this principle, if not this exact quote in mind. The result is a language which often reads like pseudo-code. For example:

  1. def buyFruit(fruit):
  2.   if fruit.color == 'red' and fruit in [Apple, Orange, Tomato]:
  3.     return fruit.price
  4.  

Flexible Semantics

Although Python primarily an object oriented/procedural language, it has functional elements including functions as first class objects, lambda expressions and map/filter/reduce functions. This gives considerable flexibility in how you go about solving problems.

An excellent summary of the design philosophy that guides both Python's code design and the style of Python programs in general see: The Zen of Python

List Comprehension

Python's list comprehension syntax is a compact and clear way to create new lists from existing lists. The syntax itself looks a lot like set comprehension notation as it's used in discrete mathematics. Here are a few examples:

Given the base list of integers:

  1. xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

You can create a list of every integer doubled with:

  1. [x * 2 for x in xs]

Or you can create a list of only odd integers with:

  1. [x for x in xs if x % 2 == 1]

Or a combination of both, doubling all of the odd integers from 1 to 10:

  1. [x * 2 for x in xs if x % 2 == 1]

Generators

Generators are a powerful addition to Python's syntax. They permit a form of lazy evaluation where they create values as they are needed rather than all at once. For example, a common way to write a function for a Sieve of Eratosthenes to generate prime numbers is:

  1. def generatePrimes(topValue):
  2.   primes = [2]
  3.   for testValue in range(3, topValue + 1):
  4.     divisors = [prime for prime in primes if testValue % prime == 0]
  5.     if divisors == []:
  6.       primes += [testValue]
  7.   return primes

The above function will, when called, generate all of the prime numbers from 2 up to the value given in the argument. The problem is that it will generate them all immediately, whether you need them or not. This is potentially a serious waste of resources if it comes out that you only needed a handful of them. Python solves this problem by using generators:

  1. def generatePrimes(topValue):
  2.   primes = [2]
  3.   for testValue in range(3, topValue + 1):
  4.     divisors = [prime for prime in primes if testValue % prime == 0]
  5.     if divisors == []:
  6.       primes += [testValue]
  7.       yield testValue

The only difference between our new function and the original is the last line. Note that in the new function there is no return statement, instead there is a 'yield' statement, and it is inside the for loop. The new function returns a generator which can be passed to any function that operates on lists and it will treat it as a linked list. Should the function that uses the generator return before all of the values have been extracted then the remaining values will never be generated. This will allow you to avoid doing work that isn't needed. Even if you find that you need all of those values, rather than having a potentially long wait at the start of the process, the work will be spread throughout the process, allowing for just in time delivery of your data.

Python's Lineage

ABC

Python was originally developed to be a replacement for ABC, and on the surface the resemblance is striking:

ABC:

  1. PUT {} IN telephone
  2. PUT 5551212 IN telephone["Spam Foo"]
  3. PUT 8674309 IN telephone["Eggs Bar"]
  4. FOR name IN keys telephone:
  5.   WRITE "Name:", name, " Phone:", telephone[name] /

Python:

  1. telephone = {}
  2. telephone["Spam Foo"] = 5551212
  3. telephone["Eggs Bar"] = 8674309
  4. for name in telephone.keys():
  5.   print "Name:", name, " Phone:", telephone[name]

Both of the above map telephone numbers to names in an associative array, then print them in the form: Name: Spam Foo Phone: 5551212 Name: Eggs Bar Phone: 8674309

In addition to the similarities in syntax, they both use an interactive environment which allows you to type your code directly into the command line and get a response from the interpreter.

Haskell

From the appearance of the syntax, it's clear that Python acquired its list comprehension functionality from Haskell.

Haskell:

  1. [oddNum * 2 | oddNum <- [1 .. 10], oddNum `mod` 2 == 1]

Python:

  1. [oddNum * 2 for oddNum in range(1, 11) if oddNum % 2 == 1]

Both of the above produce a list of integers, [2, 6, 10, 14, 18] which are the odd numbers from 1 to 10, doubled.

Python Installation

Complications to Look Out For

Currently there are two active branches in Python, 2.7 and 3.1. Python 3.1 has a number of improvements to the language which make the syntax regular and prevent some difficult to track bugs that are a serious issue in 2.7. Unfortunately it is not backwards compatible. I'll be using 2.7 for this article because it continues to have the most libraries available for it.

If you're interested in the changes made in Python 3.1, you can find them at What's New In Python 3.0.

Downloading And installing Python 2.7

You can find the latest release of Python 2.7 from Python download. From that point installation is easy, run the installer and accept the default values. The full install will take 52MB of space on your drive.

First Program

For a first program, we'll get started with "Hello World". In Python this one is so trivial that it takes more effort to run it than to write it. Copy and paste the code below into your preferred text editor and save it as 'helloworld.py':

helloworld.py:

  1. print "Hello World"

Executing the Program

The primary purpose of the hello world program is to create a simple program that demonstrates how to compile/execute a program in the given language.

To execute this one type "c:\python27\python helloworld.py" at the command line.

There is no direct use of a 'main' function in Python. Code execution starts at the beginning of the file and progresses through the end.

There are a number of ways to impose more structure on a Python program, but the purpose of this first project is to make sure our systems are configured correctly. I'll get to the more interesting stuff in next week's post.

Trivia

Python was named for the British comedy group Monty Python. Python's creator creator, Guido van Rostrum has the final say on all of Python's design decisions and is called the Benevolent Dictator for Life.

Resources

Python Wikipedia Article

Python.org

Python Download

The Zen of Python

What's New In Python 3.0

Python Cheat Sheet

Filed under: python, hello world 3 Comments
8Nov/100

Python 2/4: Loops, Decisions, and Tests

Posted by Frank Berthold

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

15Nov/100

Python 3/4: Context Managers, XML and Networking

Posted by Frank Berthold

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

Writing an RSS Aggregator with Python

For this article I'll be putting together a simple RSS aggregator. This will shed a some light on Python's XML and web libraries and introduce a way to minimize boilerplate code with context managers.

The final product will read through a file containing a list of RSS feeds and print the names of the channels and the articles they contain to screen.

To use it, type: python RssReader.py feeds.txt

Files Used in this Project

  • RssReader.txt: The sourcecode for this project, rename it to "RssReader.py" after downloading.
  • feeds.txt: A sample input file.

Libraries

  1. import sys
  2. import urllib2
  3. import xml.sax
  4. import xml.sax.handler
  5.  
  • sys: A library for accessing information passed into or maintained by Python's interpreter.
  • urllib2: A library for manipulating URL's and opening HTTP streams.
  • xml.sax: Read from and write to XML files using SAX.
    • Tutorial: A good overview of parsing XML using the SAX library.

Parsing XML

The RSS feed parser extracts the names of the channels and the titles of each of the articles.

The handler below gets most of its functionality from its parent class. There are four methods added: startDocument, startElement, endElement and characters. These each fire as an XML document is being read. Because the methods themselves are generic enough to apply to any XML document, you need a lot of if/else statements inside to deal with specific kinds of XML document.

  1. class RssHandler(xml.sax.handler.ContentHandler):
  2.   def startDocument(self):
  3.     self.inItem = False
  4.     self.inTitle = False
  5.     self.channels = {}
  6.     self.currentChannel = None
  7.     self.str = ""
  8.  
  9. def startElement(self, name, attrs):
  10.     lname = name.lower()
  11.  
  12.     if lname == "item":
  13.       self.inItem = True
  14.     elif lname == "title":
  15.       self.inTitle = True
  16.  
  17. def endElement(self, name):
  18.     lname = name.lower()
  19.  
  20.     if lname == "item":
  21.       self.inItem = False
  22.     elif lname == "title":
  23.       self.inTitle = False
  24.       if self.inItem:
  25.         self.channels[self.currentChannel] += [self.str]
  26.       else:
  27.         self.currentChannel = self.str
  28.         if self.currentChannel not in self.channels.keys():
  29.           self.channels[self.currentChannel] = []
  30.       self.str = ""
  31.  
  32.   def characters(self, content):
  33.     if self.inTitle:
  34.       self.str += content
  35.  

This class reads through an XML document and accumulates a relational list of the form: {"Channel 1":["Article 1", "Article 2"], "Channel 2":["Article 3"]}

Context Managers and Downloading Webpages

Context Managers

Files, network connections and other information that require streams of data generally require some amount of boilerplate to open and close those streams. This both adds uninteresting noise to your code and can cause unexpected bugs when you open a stream but fail to close it. Python deals with this through context managers.

Without context managers printing the lines of a file to screen looks like this:

  1. file = open("infile.txt", "r")
  2. print file.read()
  3. file.close()
  4.  

Though this is clear and obvious in a small example, in real code there can be problems between opening and closing the file, which cause it not to be closed properly. The standard fix is to wrap this with a try/finally to deal with any exceptions that are thrown in the process:

  1. file = open("infile.txt", "r")
  2. try:
  3.   print file.read()
  4. finally:
  5.   file.close()
  6.  

Where finally makes certain that, regardless of what happens in the middle, the file gets closed. This is effective, but contains a fair amount of boilerplate for a simple action. Context managers allow us to do this:

  1. with open("infile.txt", "r") as file:
  2.   print file.read()
  3.  

The open function returns a file object which contains the context manager. Any object can become a context manager object by adding an __enter__ and __exit__ method for creation and cleanup. Many of Python's standard stream objects have these methods added already.

Downloading Webpages

Unfortunately the urlopen function from urllib2 does not come with a context manager, so I had to write one myself. Fortunately, it's easy.

  1. class Url():
  2.   def __init__(self, url):
  3.     self.url = url
  4.  
  5.   def __enter__(self):
  6.     self.stream = urllib2.urlopen(self.url)
  7.     return self.stream
  8.  
  9.   def __exit__(self, type, value, traceback):
  10.     self.stream.close()
  11.  

Tying it All Together

Using the XML Parser

  1. def generateRsses(feedFile):
  2.   with open(feedFile, "r") as file:
  3.     urls = [url.strip() for url in file.readlines()]
  4.  
  5.   for url in urls:
  6.     with Url(url) as rss:
  7.       handler = RssHandler()
  8.       parser = xml.sax.make_parser()
  9.       parser.setContentHandler(handler)
  10.       parser.parse(rss)
  11.       yield handler.channels
  12.  
  13. def printFeed(rss):
  14.   for channelName in rss.keys():
  15.     print "*** " + channelName + " ***"
  16.     for title in rss[channelName]:
  17.       print "\t" + title
  18.  

The most complex part of this is the actual parsing of the xml, I'll walk you through it line by line.

  1. Create the RssHandler (the class created above). This defines how the parser will work.
  2. make_parser creates an object that does the XML heavy lifting.
  3. Attach the handler to the parser.
  4. Do the actual parsing.
  5. Extract the resulting data.

Commandline Arguments

  1. if __name__ == "__main__":
  2.   [scriptName,feedFileName] = sys.argv
  3.   for rss in generateRsses(feedFileName):
  4.       printFeed(rss)
  5.  

The only new element here is sys.argv. sys.argv contains all of the arguments handed into the python interpreter. The first is the name of the script itself, so the program ignores it, the second is the name of the file that contains a list of RSS feeds.

Resources:

Filed under: python, xml, rss No Comments
22Nov/101

Python 4/4: GUI Libraries

Posted by Frank Berthold

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

Writing a GUI for our RSS Aggregator

The Plan

I'll be finishing up the Python set with an overview of the Tkinter GUI library. There are numerous other GUI libraries available for Python, the best known of which is wxPython. I chose Tkinter because it comes built into Python and the primary goal here is to, as much as possible, look at Python as it comes out of the box.

What to Expect

When this article is complete:

  • You will have:
    • A basic GUI for last week's RSS reader.
  • You will know:
    • How to write a GUI in Python's Tkinter.
    • How to use lambda expressions to create functions on the fly.

The final GUI will look like this:

RSS Gui

Files Used in this Project

  • RssReader.txt: A library for reading RSS information, that I wrote in the previous project. Rename it to "RssReader.py" after downloading.
  • RssReaderGui.txt: The sourcecode for this project. Rename it to "RssReaderGui.py" after downloading.
  • feeds.txt: A sample input file.

The Code

Libraries

  1. from RssReader import generateRsses
  2. from Tkinter import *
  3. from tkFileDialog import askopenfilename
  4.  

Global Variables

  1. currentFeeds = {}
  2. rssDisplay = None
  3. chooseChannel = None
  4. currentChannel = None
  5. currentFeeds = None
  6.  

I've seen two ways to organizing the code for a simple one window GUI in Python. One is to wrap it all in a single large class. The other is to write a group of functions that share a set of global variables. In this case a single class would have the same problems as globals along with the overhead of adding self. to every method call and class variable.

The variables that are set to None above are place holders for global variables that will be assigned later. They don't have to be assigned here, but it's useful to know what your globals are at the front end.

Toplevel Window Creation

  1. def rssWindow():
  2.   root = Tk()
  3.   root.title("Rss Reader")
  4.   root.geometry("750x500")
  5.   root = channelFrame(root)
  6.   root = buttonFrame(root)
  7.   return root
  8.  

Although you can define your functions in arbitrary order, I've written these in a, more or less, heirarchical order. I'll walk through what I did here line by line:

  1. At the top level we create a Tk object.
  2. Give the window a title
  3. and an initial size.
  4. The frame that contains the drop down menu and list of articles.
  5. The frame that contains the select and load buttons.

The Upper Window with the Dropdown and Listbox Layout

RSS Gui

  1. def channelFrame(parent):
  2.   channelFrame = Frame(parent)
  3.   channelSelectFrame = Frame(channelFrame)
  4.   channelSelectFrame = channelLabel(channelSelectFrame)
  5.   channelSelectFrame = channelSelect(channelSelectFrame)
  6.   channelSelectFrame.pack(side=LEFT)
  7.   channelFrame = rssDisplay(channelFrame)
  8.   channelFrame.pack(side=TOP, expand=YES, fill=BOTH)
  9.   return parent
  10.  

Here we create the window that contains textual information from the RSS feeds. To accomplish this, I create frames which contain either other frames or GUI elements. Once the elements are created and organized by frame they are in, then the window behavior is set with .pack

  1. def rssDisplay(parent):
  2.   global rssDisplay
  3.   rssDisplay = Listbox(parent)
  4.   rssDisplay.pack(side=RIGHT, expand=YES, fill=BOTH)
  5.   return parent
  6.  
  7. def channelLabel(parent):
  8.   label = Label(parent, text="Select a channel:")
  9.   label.pack(side=TOP)
  10.   return parent
  11.  
  12. def channelSelect(parent):
  13.   global chooseChannel, currentChannel
  14.   currentChannel = StringVar(parent)
  15.   channelList = ["None"]
  16.   currentChannel.set(channelList[0])
  17.   chooseChannel = OptionMenu(parent, currentChannel, *channelList)
  18.   chooseChannel.pack(side=BOTTOM)
  19.   return parent
  20.  

Here the individual components are generated, the behaviors of each are described later. On line 2 I used the global keyword. You only need to use this keyword if you plan on changing the global variable, not if you're going to access it.

The Lower Window with the 'Set Config File' and 'Load Feeds' Buttons

RSS Gui

  1. def buttonFrame(parent):
  2.   buttonFrame = Frame(parent)
  3.   buttonFrame = setConfigFileButton(buttonFrame)
  4.   buttonFrame = loadRssButton(buttonFrame)
  5.   buttonFrame.pack(side=RIGHT, expand=YES, fill=X)
  6.   return parent
  7.  
  8. def setConfigFileButton(parent):
  9.   button = Button(parent, command=setConfigFile)
  10.   button["text"] = "Set Config File"
  11.   button.pack(side=LEFT)
  12.   return parent
  13.  
  14. def loadRssButton(parent):
  15.   button = Button(parent, command=loadRss)
  16.   button["text"] = "Load Feeds"
  17.   button.pack(side=RIGHT)
  18.   return parent
  19.  

Here I create a top level layout for each of the buttons then describe the specific behavior of each one. We describe the buttons behavior by passing the function to be called via the command argument.

Commands called by various elements

Lambda, functions are first class objects

  1. def loadRss():
  2.   global chooseChannel, currentChannel
  3.   chooseChannel["menu"].delete(0, END)
  4.   channelList = currentFeeds.keys()
  5.   for channelName in channelList:
  6.     chooseChannel["menu"].add_command(label=channelName,
  7.         command=lambda (temp = channelName): selectChannel(temp))
  8.   selectChannel(channelList[0])
  9.  

The loadRss function clears the values from the drop down menu then adds each of the current channels to it. It also uses a Lambda function to set the behavior of the drop down box when a given channel is selected.

Lambda functions, as used in line 7 of the above code are extraordinarily useful. It comes in handy, as in the above case, when you need to create a function for which some of the internal values are not known until runtime. Here the Lambda function's purpose is to assign the default value, channelName to the selectChannel function and assigns it as chooseChannel's command.

In general Lambda functions come in the form lambda arg, arg: arg + arg. For people who aren't used to functional it's important to remember that there's only ever one line in a lambda function and the value of that line is always returned without any need to use the return keyword.

  1. def selectChannel(channelName):
  2.   global chooseChannel, rssDisplay
  3.   chooseChannel.setvar(chooseChannel.cget("textvariable"), value = channelName)
  4.   rssDisplay.delete(0, END)
  5.   for feed in currentFeeds[channelName]:
  6.     rssDisplay.insert(END, feed)
  7.  

When one of the channels is selected from the drop down, this function fires and populates the display window with the names of the current articles.

  1. def setConfigFile():
  2.   global currentFeeds
  3.   currentFeedFile = askopenfilename(filetypes=[("allfiles", "*"),
  4.                                                 ("textfiles","*.txt")])
  5.   currentFeeds = combineFeeds(currentFeedFile)
  6.   loadRss()
  7.  

Here the program opens a file dialog with askopenfilename which will return the flie selected. It then combines all of the feeds with the same channel name and loads them into the select box to display them with loadRss.

  1. def combineFeeds(fileName):
  2.   feeds = {}
  3.   for feed in generateRsses(fileName):
  4.     for channelName in feed.keys():
  5.       if feeds.has_key(channelName):
  6.         feeds[channelName] += feed[channelName]
  7.       else:
  8.         feeds[channelName] = feed[channelName]
  9.   return feeds
  10.  

combineFeeds reads the RSS sources from the given filename, downloads the feeds using the library from the last article then combines any channels that happen to have the same name.

  1. if __name__ == "__main__":
  2.   rssWindow().mainloop()
  3.  

To actually create the window and make it useful, the program calls rssWindow(), described at top of the file, then runs the mainloop() method on it.

Final Summary

I've thoroughly enjoyed touring through Python, my observations are that Python has: * A gentle learning curve. * Many libraries with good documentation. * An easy to read syntax. * Flexible semantics.

This has been an interesting run, next week I'll be taking a look at Markdown, a lightweight markup language which allows you to create nicely typset documents from the comfort of your text editor. Starting in December I'll be taking a look through Scala a functional/object oriented language that runs in the Java VM.

Resources