Why Python programmers should learn Python

2007-07-01, , Comments

I recently clicked upon Keith Braithwaite and Ivan Moore’s presentation, “Why Java Programmers Should Learn Python”. It starts off well with an expert discussion of three different axes of type systems, placing various programming languages in the resulting 3-space. It then poses a programming problem, the kind of problem which Python excels at:

Given the supplied library classes write some code in “quickSilver” to convert between word and decimal digits representations of positive integers less than 1001.

e.g. “five seven three” → 573

e.g. 672 → “six seven two”

The Java programmers attending the presentation don’t know it yet, but “quickSilver” turns out to be Python, or at least a subset of it sufficient to solve the problem, and the final slides of the presentation contain a model solution to this problem.

A “model” solution?

Here is that solution.

class WordsFromNumber:
    def __init__(self,number):
        self.representationOf = number
    def wordsOf(self):
        lsd = self.representationOf % 10
        msds = self.representationOf / 10
        if msds == 0:
            return self.wordFor(self.representationOf)
        else:
            return WordsFromNumber(msds).wordsOf() + " " + \
                   self.wordFor(lsd)

    def wordFor(self,numeral):
        return ["zero", "one", "two", "three", "four",
                "five", "six", "seven", "eight", "nine"][numeral]


class NumberFromWords:
    def __init__(self,words):
        self.representation = words
    def number(self):
        words =  split("\\s",self.representation) 
        return self.unpack(words)
    def unpack(self,words):
        if len(words) == 1:
            return self.numberFor(words[0])
        else:
            lswi = len(words) - 1
            lswi = words[lswi]
            msws = words[0:lswi]
            return self.numberFor(lsw) + 10 * self.unpack(msw)
    def numberFor(self,word):
        return {"zero" : 0, "one" : 1, "two" : 2, "three" : 3,
                "four" : 4, "five" : 5, "six" : 6, "seven" : 7,
                "eight" : 8, "nine" : 9}[word]

I don’t know what point the presenters were trying to make. I wasn’t in their audience but if I were, this code wouldn’t go any way towards persuading me I should bother with Python. It’s got two classes where none is needed, it uses recursion when container operations would do, it’s longer than it needs to be, and (putting myself in a Java programmer’s shoes) are all those selfs really needed?

In other words this, to me, looks like Java written in Python. I’ll assume this is the point the presenters are trying to make, but if they revise the presentation, I’d like to suggest extending it to add an alternative ending, which I think shows off Python’s advantages.

An alternative ending
number_to_word = {
    '0' : 'zero', '1' : 'one', '2' : 'two', '3' : 'three', '4' : 'four',
    '5' : 'five', '6' : 'six', '7' : 'seven', '8': 'eight', '9' : 'nine'
    }
word_to_number = {
    'zero' : 0, 'one' : 1, 'two' : 2, 'three' : 3, 'four' : 4,
    'five' : 5, 'six' : 6, 'seven' : 7, 'eight' : 8, 'nine' : 9
}

def number_to_words(number):
    return ' '.join(number_to_word[c] for c in str(number))

def words_to_number(words):
    def dec_shift_add(x, y):
        return x * 10 + y
    return reduce(dec_shift_add,
        (word_to_number[w] for w in words.split()))

I’ve omitted documentation in order to squeeze the encode-decode functions on to a single slide. If I had time and space, though, I’d show how to document and test this function in one go using doctest, taking care to cover what happens with invalid inputs.

By the way, you’ll see I’m using reduce while I can! I think this example is one of those rare cases where it works well.

Dictionary initialisation

The explicit dictionaries are fine and good and almost certainly what should be presented to an audience of Python virgins, but in my own code I’d be tempted to remove a little replication. (My thanks to Ashwin for tactfully pointing out a howling bug in the code originally posted here).

Zippy dicts
import string
words = "zero one two three four five six seven eight nine".split()
number_to_word = dict(zip(string.digits, words))
word_to_number = dict(zip(words, range(10)))

Living by the Sword

A recent (2008-03-07) wave of hits from Reddit has prompted me to revisit this note and, cough, refactor my code. There’s no need for any mathematics, nested functions or reduce: this problem is better handled by string conversions. And the goal of squeezing functions on a single slide is misguided. This code needs doctesting. Java programmers are used to good test frameworks with great tool support, but Python excels at this too.

from string import digits

words = 'zero one two three four five six seven eight nine'.split()
number_to_word = dict(zip(digits, words))
word_to_number = dict(zip(words, digits))

def number_to_words(number):
    '''Converts a positive integer into a string of digit names.

    Examples:
    >>> number_to_words(123)
    'one two three'
    >>> number_to_words(-1)
    Traceback (most recent call last):
    ...
    KeyError: '-'
    '''
    return ' '.join(number_to_word[c] for c in str(number))

def words_to_number(words):
    '''Converts a string of digit names into an integer.

    Examples:
    >>> words_to_number('one two three')
    123
    >>> words_to_number('One tWo thrEE')
    123
    >>> words_to_number('zero one two')
    12
    >>> words_to_number('minus one')
    Traceback (most recent call last):
    ...
    KeyError: 'minus'
    >>> all(words_to_number(number_to_words(x)) == x for x in range(100))
    True
    '''
    return int(''.join(word_to_number[w]
                       for w in words.lower().split()))

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Feedback