Programming #9: Style and Testing

We would be remiss in our study of programming if we did not devote some time to the crafting of quality code. This subject can be a bit subjective and many programmers have a dogmatic attachment to what they believe qualifies as quality code. Luckily for us, some standards have emerged. The Python Enhancement Proposal (PEP 8) is the go-to style guide for Python code. The Google Python Style Guide is another great resource. For an interactive guide, consider Code Like a Pythonista.

This post will go through an example of crafting readable code to solve a problem. The code can be found on Github. We will stick to some best practices as summarized nicely in the Zen of Python:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one—and preferably only one—obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea—let’s do more of those!

Example problem

We’ll be using the palindrome problem as an example. We’ll design, code, and validate a solution with the focus being on readability. Let’s start the way we always do and model the problem.

Given a lowercase string s:
Write a function is_palindrome that returns True if the string is a palindrome (reads the same in reverse) and False if the string is not a palindrome.

For example:

>>> is_palindrome('racecar')
True
>>> is_palindrome('apple')
False

Finding an algorithm to solve the problem

Let’s brainstorm some solutions to the problem. The simplest algorithm would be to just brute force the solution. We could simply reverse the given string and then compare it to the original string. If the two are the same, it is a palindrome.

Can we do better? Well, instead of reversing the whole string, we could split the string into two halves and only reverse the second half. Comparing the first half to the reversed second half will give us the same solution with a bit less work.

Can we do even better? Both these solutions end up doing work that may not be necessary. For example, if the first and last letters in a string aren’t the same, we can confidently say the string is not a palindrome without any further work. Algorithmically, we compare the first letter to the last letter, if different return false, if same compare the second character to the second to last character, repeat until the middle of the string is reached. Let’s go with this.

Designing our function

When designing the function to solve this problem we’ll want to include 6 things:

  1. Examples
  2. Type Contract
  3. Header
  4. Description
  5. Body
  6. Tests

Let’s start with examples, which are what we want our function to be able to do. If we feed our function 'racecar', we want it to return True, and if we feed it 'apple' we want it to return False. We could use the doctest module to test based on these examples straight from the docstring:

def ...:
    """

    >>> is_palindrome('racecar')
    True
    >>> is_palindrome('apple')
    False
    """

Next is the type contract, which tells us the types of values, inputs and outputs, that our function will be dealing with. In this case, we will input a string and return a boolean:

def ...:
    """ (str) -> bool

    >>> is_palindrome('racecar')
    True
    >>> is_palindrome('apple')
    False
    """

Next is the header, which is the name of the function. In our case, it will be is_palindrome and take in a string s:

def is_palindrome(s):
    """ (str) -> bool

    >>> is_palindrome('racecar')
    True
    >>> is_palindrome('apple')
    False
    """

Next is the description, which tells us what the function does:

def is_palindrome(s):
    """ (str) -> bool

    Return True if and only if s is a palindrome.

    >>> is_palindrome('racecar')
    True
    >>> is_palindrome('apple')
    False
    """

And finally, before we move on to testing, the body of the function, which is where the work will be done. We also added some functionality to our script that prompts the user for a word and then returns whether it is a palindrome or not.

def is_palindrome(s):
    """ (str) -> bool

    Return True if and only if s is a palindrome.

    >>> is_palindrome('racecar')
    True
    >>> is_palindrome('apple')
    False
    """

    # Initialize position variables.
    i = 0
    j = len(s) - 1

    # Continue through positions until middle is reached.
    while i < j and s[i] == s[j]:
        # Move closer to middle
        i += 1
        j -= 1

    # If we reached the middle, return True.
    # Else return False.
    return j <= i

if __name__ == '__main__':
    word = input('Enter a word: ')
    if is_palindrome(word):
        print(word, 'is a palindrome.')
    else:
        print(word, 'is not a palindrome.')

Notice the if __name__ == '__main__': line of code. This makes sure that the script is importable without executing the script’s main functionality. This will make sure there are no issues when running unit tests.

Testing

When testing any code, it is important to consider three factors:

  1. Size of tests: empty, single, smallest interesting case, long case?
  2. Important dichotomies: vowels/non-vowels, even/odd, positive/negative, empty/full?
  3. Order: does function behave different depending on order?

For our function, we will want to consider:

  1. String lengths of 0, 1, 2, 3, 6, 7.
  2. Smallest even palindrome, smallest odd palindrome, long even, long odd.
  3. A True and False case for each.

It is important to test all these cases to make sure our function is broadly applicable. Many times, a function will work great in certain cases and fall apart for others. Our unit tests will be: ‘ ‘, ‘a’, ‘aa’, ‘ab’, ‘aba’, ‘abc’, ‘abccba’, ‘abcdef’, ‘abcdcba’, and ‘abcdefg’.

# Import test libraries
import unittest
import palindrome

# Build test class
class TestPalindrome(unittest.TestCase):
    """Example unittest test methods for is_palindrome."""

    def test_palindrome_example_1(self):
        """Test is_palindrome for empty case."""

        actual = palindrome.is_palindrome('')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_2(self):
        """Test is_palindrome for single case."""

        actual = palindrome.is_palindrome('a')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_3(self):
        """Test is_palindrome for small even true."""

        actual = palindrome.is_palindrome('aa')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_4(self):
        """Test is_palindrome for small even false."""

        actual = palindrome.is_palindrome('ab')
        expected = False
        self.assertEqual(expected, actual)

    def test_palindrome_example_5(self):
        """Test is_palindrome for small odd true."""

        actual = palindrome.is_palindrome('aba')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_6(self):
        """Test is_palindrome for small odd false."""

        actual = palindrome.is_palindrome('abc')
        expected = False
        self.assertEqual(expected, actual)

    def test_palindrome_example_7(self):
        """Test is_palindrome for long even true."""

        actual = palindrome.is_palindrome('abccba')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_8(self):
        """Test is_palindrome for long even false."""

        actual = palindrome.is_palindrome('abcdef')
        expected = False
        self.assertEqual(expected, actual)

    def test_palindrome_example_9(self):
        """Test is_palindrome for long odd true."""

        actual = palindrome.is_palindrome('abcdcba')
        expected = True
        self.assertEqual(expected, actual)

    def test_palindrome_example_10(self):
        """Test is_palindrome for long odd false."""

        actual = palindrome.is_palindrome('abcdefg')
        expected = False
        self.assertEqual(expected, actual)

if __name__ == '__main__':
    unittest.main(exit=False)

Running this module should return that 10 tests passed. There we have it, a simple program written and tested to solve a simple problem.

References:
https://www.coursera.org/learn/program-code
https://www.python.org/dev/peps/pep-0008/
http://python.net/~goodger/projects/pycon/2007/idiomatic/
https://google.github.io/styleguide/pyguide.html

Advertisements

Author: David Shahrestani

"I have the strength of a bear, that has the strength of TWO bears."

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s