Hannah Clare Wray Hazi

I love making beautiful things.

Jan 23, 2017

Stop Writing (Unecessary) Classes

I listened to a great talk recently called Stop Using Classes by Peter Diedrich. The talk goes through some of the unhelpful ways classes can be used in Python and makes good points about readability and simplicity. Here's my mindmap of the talk. My mindmap of the talk

Game of Life

Diedrich uses as an example a neat implementation of Conway's Game of Life which is beautifully straightforward and avoids the tempting trap of making a class for a Cell, a class for a Board, etc. His example is so small I've copied and annotated it here (renamed a few of the variables for extra clarity):

""" Example Game of Life from 'Stop Writing Classes' talk by Jack Diedrich. """
import itertools


def neighbours(point):
    x, y = point
    yield x+1, y-1
    yield x+1, y
    yield x+1, y+1
    yield x, y-1
    yield x, y+1
    yield x-1, y-1
    yield x-1, y
    yield x-1, y+1


def advance(board):
    new_state = set()  # initialises a set with a blank list inside
    friends = set(itertools.chain(*map(neighbours, board)))
    cells_we_care_about = board | friends

    for point in cells_we_care_about:
        count = sum((cell in board)
                    for cell in neighbours(point))
        if count == 3 or (count == 2 and point in board):
            new_state.add(point)

    return new_state


glider = set([(0,0), (1,0), (2,0), (0,1), (1,2)])

for i in range(1000):
    glider = advance(glider)

print glider

What friends does requires some unpacking. First of all, *map(neighbours, board) takes the iterable returned from map(neighbours, board) and returns it. The * is a useful way to split up the returned single thing from map into each piece and feed them separately to the chain() function as arguments. Here's an example of * in action:

def hallo(arg1, arg2):
     print(arg1)
     print(arg2)
     print("Hallo!!")

>>> printme = ("Yes", "Indeed")
>>> hallo(printme)
Traceback (most recent call last):
  File "/usr/lib/python3.5/code.py", line 91, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
TypeError: hallo() missing 1 required positional argument: 'arg2'
>>> hallo(*printme)
Yes
Indeed
Hallo!!

This map() iterable is the function neighbours() applied to every point in board. So map(neighbours, board) returns sets of all the live cells' neighbours. This is used as an argument to the chain() function. What chain() does is glues groups of iterable things together into a chain object. For example:

>>> import itertools
>>> love = itertools.chain("123", "DEF")
>>> print love
<itertools.chain object at 0x7fe58ada5810>
>>> for item in love:
...     print item
...     
1
2
3
D
E
F
>>> sling_it_into_a_set = set(love)
>>> print sling_it_into_a_set
set(['D', 'E', 'F', 1, 2, 3])
>>>

So it bungs all the sets of neighbours into one single set. Because of how Python sets work, this gets rid of any duplicate points - eg points which neighbour a couple of living cells should still only be mentioned once.

| is just Python's binary OR operator - so cells_we_care_about is a set which contains all points that are in the list of live cells OR the list of their neighbours

count counts neighbours for each living and dead cell. Diedrich then applies the rules for Conway's Game of Life in one line: A dead cell that has 3 live neighbours becomes alive A live cell that has 2 live neighbours remains alive * Every other cell dies.

Sets are awesome

One of the reasons his implementation is so tidy and has so few lines is his great use of set(), one of the built-in Python types. Sets are particularly great for this problem because they contain only unique elements - if you try to add multiples of the same element to a set you only end up with one in there. This means any duplicate points are eliminated without having to think about it.

For more examples of how to get good use out of Python's built-ins I recommend Built in Super Heroes by the excellent Dave Beazley.