Two ways to implement the Game of Life
I started my career writing Smalltalk, a language with perhaps the purest object-oriented style: everything is an object, and objects communicate by sending messages. At the time, I believed this was the natural way to represent reality. I later became more interested in choosing the smallest representation that fits the problem.
To illustrate the difference, here are two implementations of Conway’s Game of Life in Python. The first models every cell as an object.
Object-oriented version
import random
class Cell:
def __init__(self, x, y, value, field):
self.x = x
self.y = y
self.value = value
self.field = field
def next_value(self):
neighbors = self.field.get_neighbors(self.x, self.y)
neighbors_alive = sum(cell.value for cell in neighbors)
if neighbors_alive == 2:
return self.value
if neighbors_alive == 3:
return 1
return 0
class Field:
def __init__(self, height, width):
self.height = height
self.width = width
self.cells = [
[Cell(i, j, random.randint(0, 1), self) for j in range(width)]
for i in range(height)
]
def step(self):
self.cells = [
[Cell(i, j, self.cells[i][j].next_value(), self)
for j in range(self.width)]
for i in range(self.height)
]
def get_neighbors(self, x, y):
neighbors = []
for dx in range(-1, 2):
for dy in range(-1, 2):
nx = x + dx
ny = y + dy
outside = not (0 <= nx < self.height and 0 <= ny < self.width)
if outside or (dx == 0 and dy == 0):
continue
neighbors.append(self.cells[nx][ny])
return neighbors
field = Field(100, 100)
for _ in range(1000):
field.step()This version is intentionally object-heavy. A cell stores its coordinates, value, and a reference to the field. Every generation allocates a new object for every cell, while neighbor counting happens in Python loops. The representation works, but it introduces much more machinery than the rules require.
Array-oriented version
import numpy as np
from scipy.signal import convolve2d
field = np.random.randint(0, 2, size=(100, 100))
kernel = np.ones((3, 3), dtype=int)
kernel[1, 1] = 0
for _ in range(1000):
neighbors = convolve2d(
field, kernel, mode="same", boundary="fill", fillvalue=0
)
field = (neighbors == 3) | ((field == 1) & (neighbors == 2))Here the field is naturally represented as a binary matrix. Convolution with a 3 × 3 kernel counts the live neighbors of every cell; the zero in the kernel’s center excludes the cell itself. The final expression directly encodes the rules: a dead cell becomes alive with exactly three live neighbors, while a live cell remains alive with two or three.
The explicit boundary="fill" setting matches the first
implementation: cells outside the field are treated as dead.
The array-oriented version is both shorter and generally much faster because it stores values compactly and performs the repeated numerical work inside SciPy’s compiled implementation. It also exposes the structure of the problem: local neighbor aggregation followed by an element-wise rule.
This is not evidence that object-oriented programming is always slow or inappropriate. It is evidence that representation choice matters. When a problem is fundamentally about applying the same local operation to a dense grid, an array is usually a better abstraction than thousands of mutually connected objects.