Water pouring

How to solve the water pouring puzzle programmatically.

Given two jugs of capcity of 3 and 5 liters, acquire exactly 4 liters in a jug. Assume an unlimited water supply, and that jugs can only be filled or emptied, i.e., no estimations.

First to model the data: a mapping of jug sizes to their current quantity. There are 3 primitive operations:

  • filling a jug to capacity
  • emptying a jug entirely
  • pouring from a source jug to a destination jug, until either the source is emptied or the destination is full
In [1]:
class Jugs(dict):
    def fill(self, size):
        self[size] = size

    def empty(self, size):
        self[size] = 0
    
    def pour(self, src, dest):
        total = self[src] + self[dest]
        self[src] = max(total - dest, 0)
        self[dest] = min(total, dest)

That's sufficient to solve the puzzle through sheer brute force: scanning every possible combination breadth-first. Note the below implementations don't terminate unless a solution is found.

In [2]:
import itertools
from functools import partial

sizes = 3, 5
operations = list(itertools.chain(
    (partial(Jugs.fill, size=size) for size in sizes),
    (partial(Jugs.empty, size=size) for size in sizes),
    (partial(Jugs.pour, src=src, dest=dest)
         for src, dest in itertools.permutations(sizes, 2)),
))

def search(target):
    for n in itertools.count(1):
        for ops in itertools.product(operations, repeat=n):
            jugs = Jugs.fromkeys(sizes, 0)
            states = [op(jugs) or tuple(jugs.values()) for op in ops]
            if any(target in state for state in states):
                return states

%time search(4)
CPU times: user 138 ms, sys: 2.28 ms, total: 140 ms
Wall time: 144 ms
Out[2]:
[(0, 5), (3, 2), (0, 2), (2, 0), (2, 5), (3, 4)]

Now to relentlessly simplify the code. The first observation is that an empty source is useless, as is a full destination. So the fill and empty primitives are actually unneeded, and can be integrated into the pour method.

In [3]:
def pour(jugs, src, dest):
    if not jugs[src]:
        jugs[src] = src
    if jugs[dest] >= dest:
        jugs[dest] = 0
    total = jugs[src] + jugs[dest]
    jugs[src] = max(total - dest, 0)
    jugs[dest] = min(total, dest)

operations = [partial(pour, src=src, dest=dest) 
               for src, dest in itertools.permutations(sizes, 2)]

%time search(4)
CPU times: user 87 µs, sys: 0 ns, total: 87 µs
Wall time: 88.7 µs
Out[3]:
[(3, 2), (2, 0), (3, 4)]

That reduced the search space considerably, but moreover has revealed another simplification: it's pointless to "undo" a pour. Whether the source was emptied or the destination was filled, whatever reversing the previous pour direction would accomplish could have been done in the first place.

If there were more than 2 jugs, then there could be complex workflows. But with only 2, the first choice in pour directions determines the rest. There are only 2 potential solutions to the puzzle.

In [4]:
def search(target, src, dest):
    jugs = dict.fromkeys((src, dest), 0)
    while True:
        pour(jugs, src, dest)
        yield tuple(jugs.values())
        if target in jugs.values():
            return

list(search(4, 5, 3))
Out[4]:
[(2, 3), (0, 2), (4, 3)]
In [5]:
list(search(4, 3, 5))
Out[5]:
[(0, 3), (1, 5), (0, 1), (0, 4)]

And both of them are valid solutions. The solution to the puzzle is quite simply: keep pouring. It doesn't even matter which to start with.

But it can be further simplified. Now it's clear that the jug data structure is only providing modular arithmetic.

In [6]:
def search(target, src, dest):
    for n in itertools.count(1):
        quot, rem = divmod(n * src - target, dest)
        if not rem:
            return n, -quot

search(4, 5, 3)
Out[6]:
(2, -2)
In [7]:
search(4, 3, 5)
Out[7]:
(3, -1)
In [8]:
assert (5 * 2) - (3 * 2) == (3 * 3) - (5 * 1) == 4

The puzzle is looking for integer solutions to $$ \ 3x + 5y = 4 $$ Which is known as a linear Diophantine equation, and must have a solution because $4$ is a multiple of $gcd(3, 5)$.

Coin balance

How to solve the coin balance puzzle programmatically.

Given a balance and a set of coins, which are all equal in weight except for one, determine which coin is of different weight in as few weighings as possible.

Twelve-coin problem

A more complex version has twelve coins, eleven or twelve of which are identical. If one is different, we don't know whether it is heavier or lighter than the others. This time the balance may be used three times to determine if there is a unique coin—and if there is, to isolate it and determine its weight relative to the others. (This puzzle and its solution first appeared in an article in 1945.[2]) The problem has a simpler variant with three coins in two weighings, and a more complex variant with 39 coins in four weighings.

First to model the data:

  • An enum to represent different weights. Following the ternary comparison convention, such as Python 2's cmp, is convenient.
  • An object to represent the balance. For testing, it will need to be configurable with the target coin and relative weight.
  • An object to represent a coin and its state. A class is tempting, but the most useful data structure would keep the coins grouped by their known (or unknown) state anyway. So any hashable unique identifier is sufficient.
In [1]:
import enum

class Weight(enum.IntEnum):
    LIGHT = -1
    EVEN = 0
    HEAVY = 1

class Balance:
    def __init__(self, coin, weight: Weight):
        self.coin = coin
        self.weight = weight

    def weigh(self, left: set, right: set):
        """Return relative Weight of left side to right side."""
        assert len(left) == len(right)
        if self.coin in left:
            return self.weight
        if self.coin in right:
            return Weight(-self.weight)
        return Weight.EVEN

coins = 'abcdefghijkl'
assert len(coins) == 12
balance = Balance('a', Weight.LIGHT)
assert balance.weigh('a', 'b') == Weight.LIGHT
assert balance.weigh('b', 'c') == Weight.EVEN
assert balance.weigh('b', 'a') == Weight.HEAVY

As is typical with induction puzzles, the constants chosen are just large enough to thwart an iterative approach. The 2 weighing variation would be trivial enough for most people to brute force the solution. Whereas 4 weighings would already be such a large decision tree, it would be tedious to even output. The easier approach is solve the puzzle recursively and more generally, for any number of coins and weighings.

So what can be done in a single weighing? Clearly all weighings must have an equal number of coins on each side, else nothing is learned. If it balances, then the different coin is in the unweighed group. If it doesn't balance, then the different coin is in the weighed group, but additionally it is known whether each coin would be heavy or light based on which side it was on. This is the crucial insight: there's a variant recursive puzzle embedded inside this puzzle.

The Towers of Hanoi is a classic puzzle often used in computer science curricula to teach recursion. This one would suitable as a subsequent more advanced problem.

So what can be done with known coins in a single weighing? If it balances, then as before the different coin is in the unweighed group. But if it doesn't balance, then which way can be used to further narrow the coins. Consider the heavier side; the different coin must be one of the heavy ones on that side, or one of the light ones on the other side. Therefore the coins can be split into 2 equal sized groups by putting equal numbers of heavy coins on each side, and equal numbers of light coins on each side. One obstacle is that if there aren't an even number, there will need to be filler coins just to balance. But that won't be a problem after the first weighing.

Now we can implement a solution to the sub-problem, and build the need for filler coins into the balance implementation. A generator is used so that the output of each weighing can be displayed.

In [2]:
import itertools

class Balance:
    def __init__(self, coin, weight: Weight):
        self.coin = coin
        self.weight = weight
        self.filler = set()

    def weigh(self, left: set, right: set):
        """Return relative Weight of left side to right side."""
        assert abs(len(left) - len(right)) <= len(self.filler)
        if self.coin in left:
            return self.weight
        if self.coin in right:
            return Weight(-self.weight)
        return Weight.EVEN

    def find(self, light: set, heavy: set):
        """Recursively find target coin from sets of potentially light and heavy coins."""
        yield light, heavy
        union = light | heavy
        if len(union) <= 1:
            return
        left, right = set(), set()
        # split into 3 groups
        for start, third in enumerate([left, right]):
            for group in (light, heavy):
                third.update(itertools.islice(group, start, None, 3))
        weight = self.weigh(left, right)
        if weight < 0:
            light, heavy = (light & left), (heavy & right)
        elif weight > 0:
            light, heavy = (light & right), (heavy & left)
        else:
            light, heavy = (light - left - right), (heavy - left - right)
        self.filler.update(union - light - heavy)
        yield from self.find(light, heavy)

balance = Balance('a', Weight.LIGHT)
for light, heavy in balance.find(set('abc'), set('def')):
    print(''.join(light), ''.join(heavy))
cba dfe
a e
a 

Now with the sub-problem solved, there's just one thing missing for the main puzzle. In the known case, splitting into 3 equal sized groups is cleary optimal. But in the unknown case, we need to know how many coins to exclude from the weighing. This requires computing how many coins can be handled in the subsolution. Lucikly it's a trivial recurrence relation: n weighings can solve 3 times the number of n - 1 weighings. $$ \prod_{}^n 3 = 3^n $$

In [3]:
class Balance(Balance):
    def solve(self, count: int, coins):
        """Recursively find target coin."""
        if count <= 0:
            return
        weigh = set(itertools.islice(coins, 3 ** (count - 1) - (not self.filler)))
        exclude = set(coins) - weigh
        left, right = (set(itertools.islice(weigh, start, None, 2)) for start in range(2))
        weight = self.weigh(left, right)
        self.filler.update(exclude if weight else weigh)
        if weight < 0:
            yield from self.find(left, right)
        elif weight > 0:
            yield from self.find(right, left)
        else:
            yield from self.solve(count - 1, exclude)

balance = Balance('a', Weight.LIGHT)
for light, heavy in balance.solve(3, coins):
    print(''.join(light), ''.join(heavy))

for coin in coins:
    light, heavy =  list(Balance(coin, Weight.LIGHT).solve(3, coins))[-1]
    assert light == {coin} and not heavy
    light, heavy =  list(Balance(coin, Weight.HEAVY).solve(3, coins))[-1]
    assert not light and heavy == {coin}
dbah fceg
a e
a 

The puzzle is solved. There's one last simplifcation that can be made, but requires a bit more math background. Ideally we wouldn't need to know the objective number of weighings; the algorithm would just solve any set of coins as efficiently as possible. To do that, the number of coins that can be solved has to be computed. As was done above, but this recurrence relation is more advanced: each weighing can solve 3 ^ n more coins. $$ \sum_{k=0}^{n-1} 3^k = (3^n - 1) / 2 $$

With that calculation inverted, the count can be removed from the interface

In [4]:
import math

class Balance(Balance):
    def solve(self, coins):
        if not coins:
            return
        count = math.ceil(math.log(len(coins) * 2 + 1, 3))
        weigh = set(itertools.islice(coins, 3 ** (count - 1) - (not self.filler)))
        exclude = set(coins) - weigh
        left, right = (set(itertools.islice(weigh, start, None, 2)) for start in range(2))
        weight = self.weigh(left, right)
        self.filler.update(exclude if weight else weigh)
        if weight < 0:
            yield from self.find(left, right)
        elif weight > 0:
            yield from self.find(right, left)
        else:
            yield from self.solve(exclude)

balance = Balance('a', Weight.LIGHT)
for light, heavy in balance.solve(coins):
    print(''.join(light), ''.join(heavy))
dbah fceg
a e
a 

Notice the formula indicates it's possible to do 13 coins in 3 weighings, and it would be with a filler coin to balance out the 9 that need weighing.

Hat puzzle

How to solve the Hat puzzle programmatically.

Ten-Hat Variant

In this variant there are 10 prisoners and 10 hats. Each prisoner is assigned a random hat, either red or blue, but the number of each color hat is not known to the prisoners. The prisoners will be lined up single file where each can see the hats in front of him but not behind. Starting with the prisoner in the back of the line and moving forward, they must each, in turn, say only one word which must be "red" or "blue". If the word matches their hat color they are released, if not, they are killed on the spot. A friendly guard warns them of this test one hour beforehand and tells them that they can formulate a plan where by following the stated rules, 9 of the 10 prisoners will definitely survive, and 1 has a 50/50 chance of survival. What is the plan to achieve the goal?

This puzzle involves three concepts common to classic logic puzzles:

Theory of mind comes into play because each prisoner has differing knowledge, but assumes everyone else will think similarly. Functional fixedness occurs more subtly; each prisoner may state a color only to convey information. But because the information is encoded as a color, it tends to focus thinking on the colors themselves. So to combat that cognitive bias, first create a different enumeration to represent statements. Any binary enum can be mapped back to colors, so why not bool.

In [1]:
colors = 'red', 'blue'
colors[False], colors[True]
Out[1]:
('red', 'blue')

Which leaves induction: solve the puzzle for the base case (smallest size) first, and then methodically build on that solution. In the case of 1 prisoner, they have no information a priori, and therefore have a 50/50 chance of survival regardless of strategy. This variant of the puzzle already gives the optimal goal, so we know that everyone but the 1st can say their color and be saved, while the 1st can devote their answer to the common cause.

In the case of 2 prisoners, obviously the 1st can say the color of the 2nd. That approach does not scale; it is the path to functional fixedness. Instead, methodically enumerate all possible statements and colors to determine if there is an unambiguous solution.

In [2]:
table = list(zip([False, True], colors))
table
Out[2]:
[(False, 'red'), (True, 'blue')]

The above table is a general solution with no assumptions other than the arbitrary ordering of enums. While it may appear absurdly pedantic, it represents a rule set which is key to building a recursive solution.

In the case of the 3rd prisoner, clearly they can not just repeat the above rule set, because the 3rd would receive no information. But there are only 2 choices, so the only option is to follow the opposite rule set, depending on the 3rd color.

The crucial step is to build off of the existing table.

In [3]:
table = [row + colors[:1] for row in table] + [(not row[0],) + row[1:] + colors[1:] for row in table]
table
Out[3]:
[(False, 'red', 'red'),
 (True, 'blue', 'red'),
 (True, 'red', 'blue'),
 (False, 'blue', 'blue')]

The solution is valid if each prisoner is able to narrow the possibilities to a unique row based on the colors they hear and see.

In [4]:
import collections

def test(table):
    """Assert that the input table is a valid solution."""
    (size,) = set(map(len, table))
    for index in range(size):
        counts = collections.Counter(row[:index] + row[index + 1:] for row in table)
        assert set(counts.values()) == {1}

test(table)

The general solution is simply the above logic in recursive form, with a parametrized size.

In [5]:
def solve(count: int):
    """Generate a flat table of all spoken possibilities."""
    if count <= 1:
        yield False,
        return
    for row in solve(count - 1):
        yield row + colors[:1]
        yield (not row[0],) + row[1:] + colors[1:]

list(solve(3))
Out[5]:
[(False, 'red', 'red'),
 (True, 'red', 'blue'),
 (True, 'blue', 'red'),
 (False, 'blue', 'blue')]

The complicated puzzle is actually a trivial recurrence relation. $$ 2^n = 2^{n-1} * 2 $$ There are $2^n$ states of the prisoners, and each prisoner has $n-1$ bits of data. So an additional bit of data from the first is sufficient to solve the puzzle.

In [6]:
table = list(solve(10))
test(table)
len(table)
Out[6]:
512
In [7]:
table[:3]
Out[7]:
[(False, 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'red'),
 (True, 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'blue'),
 (True, 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'blue', 'red')]

The puzzle is solved, but the output is of exponential size, certainly not the succinct solution which makes the puzzle famous. But instead of relying on a flash of insight, this approach produces not just a solution, but the solution. The only arbitrary decision made was the enumeration. Therefore it must be the case that the solution can be summarized.

First, it would be helpful to group the solution by the 1st statement. Any summary function would have to ensure that there is no collision in the grouped possibilities.

In [8]:
groups = collections.defaultdict(set)
for row in table:
    groups[row[0]].add(row[1:])
groups = groups[False], groups[True]

def summarize(func, groups):
    """Apply summary function to groups and assert uniqueness."""
    groups = tuple(set(map(func, group)) for group in groups)
    assert set.isdisjoint(*groups)
    return groups

assert summarize(lambda g: g, groups) == groups
tuple(map(len, groups))
Out[8]:
(256, 256)

Now what summaries to attempt? Well there are few properties of sequences to work with: size and order. They are all the same size, so that won't help. That leaves ordering, which can be easily tested by sorting.

In [9]:
summarize(lambda g: tuple(sorted(g)), groups)
Out[9]:
({('blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'red'),
  ('blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'red', 'red', 'red'),
  ('blue', 'blue', 'blue', 'blue', 'red', 'red', 'red', 'red', 'red'),
  ('blue', 'blue', 'red', 'red', 'red', 'red', 'red', 'red', 'red'),
  ('red', 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'red')},
 {('blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue'),
  ('blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'blue', 'red', 'red'),
  ('blue', 'blue', 'blue', 'blue', 'blue', 'red', 'red', 'red', 'red'),
  ('blue', 'blue', 'blue', 'red', 'red', 'red', 'red', 'red', 'red'),
  ('blue', 'red', 'red', 'red', 'red', 'red', 'red', 'red', 'red')})

Success. Now that order does not matter, the appropriate data structure is a multiset (a.k.a. bag). Each prisoner can keep track of only how many of each color they hear and see.

In [10]:
summarize(lambda g: frozenset(collections.Counter(g).items()), groups)
Out[10]:
({frozenset({('blue', 8), ('red', 1)}),
  frozenset({('blue', 2), ('red', 7)}),
  frozenset({('blue', 6), ('red', 3)}),
  frozenset({('blue', 4), ('red', 5)}),
  frozenset({('red', 9)})},
 {frozenset({('blue', 1), ('red', 8)}),
  frozenset({('blue', 7), ('red', 2)}),
  frozenset({('blue', 5), ('red', 4)}),
  frozenset({('blue', 3), ('red', 6)}),
  frozenset({('blue', 9)})})

Since there are only 2 colors which sum to a constant, keeping track of just one is sufficient.

In [11]:
summarize(lambda g: g.count(colors[0]), groups)
Out[11]:
({1, 3, 5, 7, 9}, {0, 2, 4, 6, 8})

There's one last pattern to the numbers, which can be used to achieve parity with the canonical solution.

Split an Iterable

Split an iterable into equal sized chunks.

A common task and interview question, with many variants. It's frequently asked and answered in a way that's suboptimal and only handles one specific case. The goal here is to present definitive, general, and efficient solutions.

The first variant is whether or not the chunks will overlap. Although this could be generalized into a step parameter, it's nearly always the case that step in (1, size).

The second variant is whether the tail of the data should be returned, if it's not of equal size. Clearly when step == 1 that's unlikely to be desirable. However, when step == size, that would seem to be the natural choice. And again when 1 < step < size, the desired behavior isn't clear at all.

The third variant is whether to slice sequences, or support any iterable. Obviously working for any iterable would be ideal, but it's also likely a user would expect slices given a sequence, particularly in the case of strings.

So this author feels it's best to split the problem in 2 distinct cases: a sliding window for overlapping sequences, and chunks for discrete sequences. In each case, supporting iterables and using advanced iterator algebra for a minimal and efficient solution.

Window

In [1]:
import itertools

def window(iterable, size=2):
    """Generate a sliding window of values."""
    its = itertools.tee(iterable, size)
    return zip(*(itertools.islice(it, index, None) for index, it in enumerate(its)))

list(window('abcde'))
Out[1]:
[('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e')]

That's simple, and close to optimal. There is slight overhead in iterating an islice object, so a minor variant would be to force the step-wise iteration in advance.

In [2]:
import collections

def window(iterable, size=2):
    """Generate a sliding window of values."""
    its = itertools.tee(iterable, size)
    for index, it in enumerate(its):
        collections.deque(itertools.islice(it, index), 0)  # exhaust iterator
    return zip(*its)

list(window('abcde'))
Out[2]:
[('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e')]

Chunks

A lesser-known and under-utilized feature of iter is that in can take a callable (of no arguments) and a sentinel to create an iterator. A perfect use case of the "loop and a half" idiom.

In [3]:
def chunks(iterable, size):
    """Generate adjacent chunks of data"""
    it = iter(iterable)
    return iter(lambda: tuple(itertools.islice(it, size)), ())

list(chunks('abcde', 3))
Out[3]:
[('a', 'b', 'c'), ('d', 'e')]

This should also be optimal, for reasonable sizes.

Sequences with dispatch

Rather than explicitly check isinstance, this is a perfect use case for functools.singledispatch.

In [4]:
import functools
from collections.abc import Sequence

window = functools.singledispatch(window)

@window.register
def _(seq: Sequence, size=2):
    for index in range(len(seq) - size + 1):
        yield seq[index:index + size]

list(window('abcde'))
Out[4]:
['ab', 'bc', 'cd', 'de']
In [5]:
list(window(iter('abcde')))
Out[5]:
[('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'e')]
In [6]:
chunks = functools.singledispatch(chunks)

@chunks.register
def _(seq: Sequence, size):
    for index in range(0, len(seq), size):
        yield seq[index:index + size]

list(chunks('abcde', 3))
Out[6]:
['abc', 'de']
In [7]:
list(chunks(iter('abcde'), 3))
Out[7]:
[('a', 'b', 'c'), ('d', 'e')]

Accumulator

A Paul Graham classic, the accumulator function.

As an illustration of what I mean about the relative power of programming languages, consider the following problem. We want to write a function that generates accumulators-- a function that takes a number n, and returns a function that takes another number i and returns n incremented by i.

(That's incremented by, not plus. An accumulator has to accumulate.)

In Common Lisp this would be

(defun foo (n) (lambda (i) (incf n i)))

...

If you try to translate the Lisp/Perl/Smalltalk/Javascript code into Python you run into some limitations. Because Python doesn't fully support lexical variables, you have to create a data structure to hold the value of n. And although Python does have a function data type, there is no literal representation for one (unless the body is only a single expression) so you need to create a named function to return. This is what you end up with:

def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar

Python users might legitimately ask why they can't just write

def foo(n): return lambda i: return n += i

or even

def foo(n): lambda i: n += i

and my guess is that they probably will, one day. (But if they don't want to wait for Python to evolve the rest of the way into Lisp, they could always just...)

There are other solutions, using function attributes or instances with a __call__ method, but none are substantially more elegant. The challenge predates Python 3, which introduced the nonlocal keyword, making this the presumably preferred solution:

In [1]:
def foo(n):
    def inc(x):
        nonlocal n
        n += x
        return n
    return inc

acc = foo(0)
acc(1)
acc(2)
Out[1]:
3

There was also yet another alternative as of Python 2.6: using a generator as a coroutine.

In [2]:
def foo(n):
    while True:
        n += yield n

acc = foo(0)
next(acc)
acc.send(1)
acc.send(2)
Out[2]:
3

To satisfy the challenge, that would need to be wrapped with a decorator. The triple partial expression below may seem a little obtuse, but it's not as bad as it looks. Just unwind it one step at a time.

In [3]:
from functools import partial

@partial(partial, partial)
def coroutine(func, *args, **kwargs):
    gen = func(*args, **kwargs)
    next(gen)
    return gen.send

coroutine
Out[3]:
functools.partial(<class 'functools.partial'>, <function coroutine at 0x10bf970e0>)
In [4]:
@coroutine
def foo(n):
    while True:
        n += yield n

foo
Out[4]:
functools.partial(<function coroutine at 0x10bf970e0>, <function foo at 0x10bf7eb90>)
In [5]:
acc = foo(0)
acc
Out[5]:
<function generator.send>
In [6]:
acc(1)
acc(2)
Out[6]:
3

But what's the most Pythonic solution? This author would argue... don't. In my experience, I have never really needed global or nonlocal in production code. Typically it's because the objects in question are mutable, so it's not necessary to rebind a name in a different scope to a new object.

A typical tell of these kinds of code challenges are that they focus on the interface or implementation exclusively, never both in context. Python numbers are immutable, and have syntactic support for incrementing, so there's nothing more readable about acc(...) instead of n += ....

Futhermore, the accumulator object is intended to be used repeatedly, such as in a loop. In a language with such strong iteration support as Python, it's extremely likely that accumulation will occur in a iterative loop. Indeed, the real accumulator has since been added to the standard library.

In [7]:
import itertools

list(itertools.accumulate(range(10)))
Out[7]:
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Map and Filter

Contrarian view on map and filter.

Although PEP 8 is silent on the topic, it's become recommended in many Python circles to eschew map and filter in favor of generator expressions or list comprehensions. For example, this Stack Overflow question received and accepted a typical response. Ironically, that question misquoted the google style guide, which this author happens to agree with.

Use list comprehensions and for loops instead of filter and map when the function argument would have been an inlined lambda anyway. [emphasis added]

The style guide also shows a non-lambda version as a positive example:

map(math.sqrt, data) # Ok. No inlined lambda expression.

First, a brief history of how the Python community arrived at this state.

Python 1

Prior to version 2.0, Python had neither list comprehensions nor nested scopes. Therefore simple map and filter operations had to use a for... append loop, or lambda. But the lacked of nested scopes was inherently crippling to the latter approach.

In [1]:
x = 2
map(lambda y: y * x, range(5))
Out[1]:
<map at 0x108e57250>

A NameError would have been raised on x, because it's not defined in the inner scope. One clever work-around was to shadow default arguments.

In [2]:
x = 2
list(map(lambda y, x=x: y * x, range(5)))
Out[2]:
[0, 2, 4, 6, 8]

Unsurprisingly, that was widely viewed as an ugly hack. Many resigned themselves to for... append loops instead.

Python 2

Then Python added list comprehensions in 2.0, and that became the one obvious way to do it.

In [3]:
x = 2
[y * x for y in range(5)]
Out[3]:
[0, 2, 4, 6, 8]

Python acquired nested scopes in the next version, 2.1, but the damage was done. Functional programming in Python in general, and lambda in particular, was widely frowned upon. Even though the lack of nested scopes affected all inner functions used in any context; it was never really about lambda per se.

Python 3

It's sometimes claimed to this day that map and filter only exist for backwards compatibility. But that belies the history of Python 3. map, filter, and reduce were all considered for removal. But only reduce was banished to the functools module. map and filter were not only retained, but updated to return iterators.

So it's already dubious to claim that using a built-in is unapproved. But the real point is that map and filter remain a higher level abstraction. Sure, with lambda there are the same number of logical components, and it's just a matter of syntactic sugar. But there is some abstraction value when the functions already have a name.

It's also commonly pointed out that generator expressions are superior because they can do a map and filter simultaneously, but crucially only if the filter comes first. Consider this task: normalizing an iterable of strings.

In [4]:
values = 'sample ', ' '
list(filter(None, map(str.strip, values)))
Out[4]:
['sample']

Note list is only being used for printing, and should be ignored for the sake of comparisons.

As for the alternative, surely calling strip twice to use a single expression is just plain cheating. So really the only option is:

In [5]:
[value for value in (value.strip() for value in values) if value]
Out[5]:
['sample']

Some would consider nested comprehensions already enough to separate with a temporary name. But that's inadvertently acknowledging how much more verbose it is.

Can it really be claimed that the latter is more readable than the former? It's just boilerplate, which never seems acknowledged in small examples. But if one only has to double the size of the context to show how verbose comprehensions are, doesn't that demonstrate the value of map and filter.

Epilogue

And now a shameless plug of the author's placeholder package for readers who appreciate function-style programming. It provides syntactic sugar for lambda.

In [6]:
from placeholder import _

list(map(_ * 2, range(5)))
Out[6]:
[0, 2, 4, 6, 8]

But even speaking as the author, map isn't the best use case. Sort keys are a much better example, since there is no competing syntax.

In [7]:
min(['ab', 'ba'], key=_[-1])
Out[7]:
'ba'

Python 3.8's new assignment expressions provide yet another alternative.

Fizz Buzz

The infamously simple FizzBuzz problem.

Reportedly a high percentage of programmer applicants can't solve this quickly.

Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.

A deep dive on this problem has been done in jest many times, e.g., deliberate over-engineering or code golf. But in all seriousness, let's consider what's the most Pythonic solution. A truncated version of the common solution:

In [1]:
for num in range(1, 16):
    if num % 5 == 0 and num % 3 == 0:
        print('FizzBuzz')
    elif num % 3 == 0:
        print('Fizz')
    elif num % 5 == 0:
        print('Buzz')
    else:
        print(num)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

Naturally interview questions tend to focus on output, e.g. print, but that's no reason to skip over basic abstractions or data structures. First, this could be written as a generator, to decouple the print operation and parametrize the numeric range. Alternatively, Python has such strong iterator support that it could also be just a function, ready to be mapped. So let's reframe the basic solution as:

In [2]:
def fizzbuzz(stop):
    for num in range(1, stop):
        if num % 5 == 0 and num % 3 == 0:
            yield 'FizzBuzz'
        elif num % 3 == 0:
            yield 'Fizz'
        elif num % 5 == 0:
            yield 'Buzz'
        else: 
            yield str(num)

' '.join(fizzbuzz(16))
Out[2]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

Even at this size, it's already violating DRY, or the Rule of 3. Clearly the same logic is being repeated with different data.

In [3]:
def fizzbuzz(stop):
    items = (15, 'FizzBuzz'), (3, 'Fizz'), (5, 'Buzz')
    for num in range(1, stop):
        yield next((text for div, text in items if num % div == 0), str(num))

' '.join(fizzbuzz(16))
Out[3]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

However, that variation had to introduce the concept of the least common multiple. Even in such a trivial problem, there's a subtlety in how one interprets requirements. The final directive to output "FizzBuzz" can be seen as a mere clarification of the previous directives; certainly not a coincidence. Making this the more obvious solution:

In [4]:
def fizzbuzz(stop):
    for num in range(1, stop):
        text = ''
        if num % 3 == 0:
            text += 'Fizz'
        if num % 5 == 0:
            text += 'Buzz'
        yield text or str(num)

' '.join(fizzbuzz(16))
Out[4]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

Arguably that insight is more important, because its duplication grows exponentially, not linearly. There's a 2**N sized case statement to handle N cases, luckily N == 2. Adding just one more directive for the number 7 would make the basic solution unwieldy.

And of course both approaches can be combined.

In [5]:
def fizzbuzz(stop):
    items = (3, 'Fizz'), (5, 'Buzz')
    for num in range(1, stop):
        yield ''.join(text for div, text in items if num % div == 0) or str(num)

' '.join(fizzbuzz(16))
Out[5]:
'1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz'

So is that over-engineered? This author would argue that both deduplication and decoupling logic from data are worth observing. So maybe at this size the final version isn't necessary, but surely the basic version is not the most Pythonic.

Cheryl's Birthday

How to solve the Cheryl's Birthday puzzle programmatically.

  1. Albert and Bernard just became friends with Cheryl, and they want to know when her birthday is. Cheryl gave them a list of 10 possible dates:
     May 15     May 16     May 19
    June 17    June 18
    July 14    July 16
    August 14  August 15  August 17
  2. Cheryl then tells Albert and Bernard separately the month and the day of the birthday respectively.
  3. Albert: I don't know when Cheryl's birthday is, but I know that Bernard does not know too.
  4. Bernard: At first I don't know when Cheryl's birthday is, but I know now.
  5. Albert: Then I also know when Cheryl's birthday is.
  6. So when is Cheryl's birthday?

As with the pytudes solution, the goal is to solve the puzzle in code. A different approach is taken here though, for simplicity and extensibility.

The first step is to model the data. A set of Date objects is suitable to represent the current possible dates. Since datetime.date objects require a year, a minimal collections.namedtuple is used instead.

In [1]:
from typing import NamedTuple

DATES = ['May 15',    'May 16',    'May 19',
        'June 17',   'June 18',
        'July 14',   'July 16',
      'August 14', 'August 15', 'August 17']

class Date(NamedTuple):
    month: str
    day: str

    def __repr__(self):
        return ' '.join(self)  # pretty printing
DATES = {Date(*date.split()) for date in DATES}
DATES
Out[1]:
{August 14,
 August 15,
 August 17,
 July 14,
 July 16,
 June 17,
 June 18,
 May 15,
 May 16,
 May 19}

As is typical of these kinds of puzzles, it assumes all participants have perfect Theory of Mind. That is, each participant making a statement is applying their private knowledge to what is publicly known, and assuming everyone else will do the same. With that in mind, the claims made fall into 3 categories:

  • I know ...
  • I don't know ...
  • They don't know ...

The temporal variations "now" and "at first" can be modeled by the current set of dates. Any claim then can be implemented functionally in this form:

In [2]:
def claim(field: str, dates: set) -> set:
    """Return subset of possible dates which would make the claim true.
    
    :param field: the field known by the claimant
    :param dates: the current set of dates publicly known
    """

So what does it mean for Albert or Bernard to "know" the correct date? It would mean applying their knowledge of the month or day leaves only one possibility. The "I know ..." function therefore groups and filters for uniqueness.

In [3]:
import collections

def known(field, dates):
    """Return subset of possible dates which would make the claim "I know ..." true."""
    counts = collections.Counter(getattr(date, field) for date in dates)
    return {date for date in dates if counts[getattr(date, field)] == 1}

# test what is already publicly known
assert known('month', DATES) == set()
known('day', DATES)
Out[3]:
{June 18, May 19}

To implement "I don't know ...", known could be parametrized with a different predicate (> 1), or simply use set.difference. "I don't know ..." is so trivial it's arguably not worth the abstraction.

In [4]:
def unknown(field, dates):
    """Return subset of possible dates which would make the claim "I don't know ..." true."""
    return dates - known(field, dates)

The challenging part is what does it mean for Albert to claim Bernard doesn't know. All dates that would be knowable to Bernard must clearly be excluded, but Albert would have to exclude them based on his knowledge of the month. So "They don't know ..." is similar to unknown, but the exclusion is based on a different field.

In [5]:
def unknowable(field, dates):
    """Return subset of possible dates which would make the claim "They don't know ..." true."""
    other, = set(Date._fields) - {field}
    exclude = {getattr(date, field) for date in known(other, dates)}
    return {date for date in dates if getattr(date, field) not in exclude}

This is sufficient to simply walk through the statements, one at a time.

In [6]:
# Albert: I don't know when Cheryl's birthday is, but I know that Bernard does not know too.
dates = unknown('month', DATES)
assert dates == DATES  # already public known
dates = unknowable('month', dates)
dates
Out[6]:
{August 14, August 15, August 17, July 14, July 16}
In [7]:
# Bernard: At first I don't know when Cheryl's birthday is, but I know now.
assert dates.isdisjoint(known('day', DATES))  # already claimed by Albert
dates = known('day', dates)
dates
Out[7]:
{August 15, August 17, July 16}
In [8]:
# Albert: Then I also know when Cheryl's birthday is.
known('month', dates)
Out[8]:
{July 16}

Exactly one date is left, indicating success. Now the succinct in-lined version, with no superfluous statements.

In [9]:
known('month',                        # Albert: I know
    known('day',                      # Bernard: I know
        unknowable('month', DATES)))  # Albert: Bernard doesn't know
Out[9]:
{July 16}