Code Challenge 01 - Word Values Part I - Review

PyBites, Fri 13 January 2017, Challenges

code review, codechallenges, generators, github, HN, learning, max, refactoring, scrabble

Thanks for coding with us!

Wow! We have been amazed by the great response on github and HN. It's so cool to see many developers jump on this :)

This is awesome!

awesome response on github, 70 forks as of this writing

It's Friday so we review the code challenge of this week. We describe our learning, a possible solution. We will also digest comments left on the Monday post.

Process and learning

>>> Julian

It's funny, going into this challenge I actually thought it was going to be easy! I was wrong! The concept was simple enough and I had a decent idea as to how I was going to write the program. I hit a wall however, when I realised I had to code my answer within the framework of the unittest.

As a newbie programmer, having to almost "restrict" my code to work with the unittest was quite difficult. Furthermore, working with the external data.py file also added a little complexity. I'm definitely used to having all data and variables located in the local file I'm working on.

Probably the biggest pain point was trying to work with the LETTER_SCORES dict:

LETTER_SCORES = {letter: score for score, letters in scrabble_scores
                              for letter in letters.split()}

The for loop within the dict threw me off completely and I spent what felt like hours trying to make sense of it. It wasn't until Bob expanded it out into multiple lines of code that it finally made sense.

On the flip side, I was pleasantly surprised with myself when I got the load_words() function working. I recalled Bob's comment on my code that I could use 'with' (context manager) to open an external file. Doing this made it much simpler.

Working on the max_word_value() function was equally as satisfying as it was much more familiar coding ... but that may not be a good thing.

In the end I wasn't actually able to get the program working. Not my proudest moment but definitely an eye opener as to how much further I have to go with my code. I'll hopefully have time this weekend to take another look - maybe a fresh look after a day off will highlight something I missed earlier!

My code is here if you're interested! Be gentle!

Going forward with these challenges, I think we'll try and shake it up a little. Not make it "mandatory" to code the program within the unittest framework which should allow us to get a more diverse code base from the community.

Overall, while difficult for me and even frustrating at times, I definitely enjoyed the challenge. It forced me to learn to read code I'd never seen before and rethink the way I write it myself.

Possible solution and Python idioms

>>> Bob

This was a good exercise. As Julian said we might leave out unittests next time to make it less stringent and make up other requirements like max LOC. We also will provide two template files: beginner (more hand-holding) and advanced (almost blank file). You will see it on Monday ...

My code is here. Some comments:

load_words()

def load_words():
    with open(DICTIONARY) as f:
        return [word.strip() for word in f.read().split()]

Yes, "with" is the way to go to open files. Initially I had return f.read().split() but then I saw the comment of sesh00: he used a list comprehension to make sure each word had whitespace stripped which is a good approach.

calc_word_value(word)

def calc_word_value(word):
    return sum(LETTER_SCORES.get(char.upper(), 0) for char in word)

The dictionary. You can access values by using letter keys as LETTER_SCORES['A'] etc, but what if there is a non-valid character? There were two words with '-' in it so they would cause a KeyError. Using the dict get() method you can give it a default value of 0. Safety first:

$ grep [^A-Za-z] dictionary.txt 
Jean-Christophe
Jean-Pierre
>>> word = 'Jean-Christophe'
>>> from data import LETTER_SCORES
>>> [LETTER_SCORES[c.upper()] for c in word]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
KeyError: '-'
>>> [LETTER_SCORES.get(c.upper(), 0) for c in word]
[8, 1, 1, 1, 0, 3, 4, 1, 1, 1, 1, 1, 3, 4, 1]

For another more verbose (cleaner?) way to write this see here:

scores = [LETTER_SCORES[letter] for letter in letters 
            if letter in LETTER_SCORES.keys()]

Then I use sum() to add up all letter values. You can give it a list comprehension but also a generator which is best practice (lazy loading):

# sum with list comprehension
>>> sum([LETTER_SCORES.get(c.upper(), 0) for c in word])
31
# or with a generator, just drop the []
>>> sum(LETTER_SCORES.get(c.upper(), 0) for c in word)
31

Of course you can totally write just a for loop and sum to a total variable. And as a beginner I encourage you to actually do this to get a feel for how an iterator works internally.

max_word_value(words)

def max_word_value(words=None):
    return max(words or load_words(), key=lambda w: calc_word_value(w))

This might be advanced to a beginner. To pass the unittests you have to account for two scenarios:

The max builtin calculates the max of an iterator, very convenient here. The cool thing is that it takes a key optional argument (like the sorted() builtin) which you can give a function to 'max on'.

In this case I don't want to max on for example len of word, but on the word value, so we re-use calc_word_value() here. For more details on this I recommend reading this great article.


Update 16th of Oct 2018: this code got outdated, we later updated the solution to not use the lambda (thanks for the reminder comment =º.º=) because it is redundant here:

def max_word_value(words=None):
    if words is None:
            words = load_words()
        return max(words, key=calc_word_value)

And even shorter is using this one-liner:

def max_word_value(words=None):
    return max(words or load_words(), key=calc_word_value)

PyBites digest of comments on Monday's challenge post

Thanks for your comments. We are really stoked to learn about all these different approaches. Also you cannot read enough other developers' code, it's a great way to learn fast!

PyBites Python Tips

Do you want to get 250+ concise and applicable Python tips in an ebook that will cost you less than 10 bucks (future updates included), check it out here.

Get our Python Tips Book

"The discussions are succinct yet thorough enough to give you a solid grasp of the particular problem. I just wish I would have had this book when I started learning Python." - Daniel H

"Bob and Julian are the masters at aggregating these small snippets of code that can really make certain aspects of coding easier." - Jesse B

"This is now my favourite first Python go-to reference." - Anthony L

"Do you ever go on one of those cooking websites for a recipe and have to scroll for what feels like an eternity to get to the ingredients and the 4 steps the recipe actually takes? This is the opposite of that." - Sergio S

Get the book