DIY with generators
Here's one way to calculate a product of lists without using the built-in
def product (*iters):
def loop (prod, first = [], *rest):
if not rest:
for x in first:
yield prod + (x,)
else:
for x in first:
yield from loop (prod + (x,), *rest)
yield from loop ((), *iters)
for prod in product ("ab", "xyz"):
print (prod)
# ('a', 'x')
# ('a', 'y')
# ('a', 'z')
# ('b', 'x')
# ('b', 'y')
# ('b', 'z')
In python, we can collect the outputs of a generator in a list by using the list constructor. Note we can also calculate the product of more than two inputs as seen below
print (list (product ("+-", "ab", "xyz")))
# [ ('+', 'a', 'x')
# , ('+', 'a', 'y')
# , ('+', 'a', 'z')
# , ('+', 'b', 'x')
# , ('+', 'b', 'y')
# , ('+', 'b', 'z')
# , ('-', 'a', 'x')
# , ('-', 'a', 'y')
# , ('-', 'a', 'z')
# , ('-', 'b', 'x')
# , ('-', 'b', 'y')
# , ('-', 'b', 'z')
# ]
Because product accepts a a list of iterables, any iterable input can be used in the product. They can even be mixed as demonstrated below
print (list (product (['@', '%'], range (2), "xy")))
# [ ('@', 0, 'x')
# , ('@', 0, 'y')
# , ('@', 1, 'x')
# , ('@', 1, 'y')
# , ('%', 0, 'x')
# , ('%', 0, 'y')
# , ('%', 1, 'x')
# , ('%', 1, 'y')
# ]
Because product is defined as a generator, we are afforded much flexibility even when writing more complex programs. Consider this program that finds right triangles made up whole numbers, a Pythagorean triple. Also note that product allows you to repeat an iterable as input as see in product (r, r, r) below
def is_triple (a, b, c):
return a * a + b * b == c * c
def solver (n):
r = range (1, n)
for p in product (r, r, r):
if is_triple (*p):
yield p
print (list (solver (20)))
# (3, 4, 5)
# (4, 3, 5)
# (5, 12, 13)
# (6, 8, 10)
# (8, 6, 10)
# (8, 15, 17)
# (9, 12, 15)
# (12, 5, 13)
# (12, 9, 15)
# (15, 8, 17)
Implementing your coin tossing program is easy now.
def toss_coins (n):
sides = [ 'H', 'T' ]
coins = [ sides ] * n
yield from product (*coins)
print (list (toss_coins (2)))
# [ ('H', 'H'), ('H', 'T'), ('T', 'H'), ('T', 'T') ]
print (list (toss_coins (3)))
# [ ('H', 'H', 'H'), ('H', 'H', 'T'), ('H', 'T', 'H'), ('H', 'T', 'T'), ('T', 'H', 'H'), ('T', 'H', 'T'), ('T', 'T', 'H'), ('T', 'T', 'T') ]
Without generators
But generators are a very high-level language feature and we wonder how we could represent product using pure recursion. Below product is reimplemented without the use of generators and now returns a filled array with all calculated sub-products
def map (f, lst):
if not lst:
return []
else:
first, *rest = lst
return [ f (first ) ] + map (f, rest)
def flat_map (f, lst):
if not lst:
return []
else:
first, *rest = lst
return f (first) + flat_map (f, rest)
def product (*iters):
def loop (acc, iters):
if not iters:
return acc
else:
first, *rest = iters
return flat_map (lambda c: map (lambda x: [x] + c, first), loop (acc, rest))
return loop ([[]], iters)
We can now skip the yield and list calls in your program
def toss_coins (n):
sides = [ 'H', 'T' ]
coins = [ sides ] * n
return product (*coins)
print (toss_coins (2))
# [('H', 'H'), ('H', 'T'), ('T', 'H'), ('T', 'T')]
print (toss_coins (3))
# [('H', 'H', 'H'), ('H', 'H', 'T'), ('H', 'T', 'H'), ('H', 'T', 'T'), ('T', 'H', 'H'), ('T', 'H', 'T'), ('T', 'T', 'H'), ('T', 'T', 'T')]
Above, we define map and flat_map with as few dependencies as possible, however there is only one subtle distinction in each implementation. Below, we represent each as a fold (using reduce) allowing us to see the semantic difference more easily. Also note Python includes its own version of map and reduce (in functools) that differ slightly from the versions provided here.
def concat (xs, ys):
return xs + ys
def append (xs, x):
return xs + [ x ]
def reduce (f, init, lst):
if not lst:
return init
else:
first, *rest = lst
return reduce (f, f (init, first), rest)
def map_reduce (m, r):
return lambda acc, x: r (acc, m (x))
def map (f, lst):
return reduce (map_reduce (f, append), [], lst)
def flat_map (f, lst):
return reduce (map_reduce (f, concat), [], lst)
def product (*iters):
# this stays the same