I might remove the extra generator wrappers, i.e., not do yield from genexp
. Bit faster.
A benchmark with input alist = [(range, 300)] * 2
:
1.70 ± 0.01 ms Stefan3
8.52 ± 0.12 ms Stefan2
8.52 ± 0.18 ms Stefan1
33.01 ± 0.35 ms Stefan4
40.47 ± 0.44 ms Karlish_no_wrap
48.25 ± 0.36 ms Karlish
121.88 ± 0.63 ms Karl
Python: 3.11.4 (main, Sep 9 2023, 15:09:21) [GCC 13.2.1 20230801]
Stefan3
is the 2D one, so can’t participate in input alist = [(range, 10)] * 5
:
13.35 ± 0.11 ms Stefan2
13.37 ± 0.10 ms Stefan1
46.54 ± 0.66 ms Stefan4
102.34 ± 2.39 ms Karlish_no_wrap
138.21 ± 1.05 ms Karlish
233.47 ± 1.98 ms Karl
Python: 3.11.4 (main, Sep 9 2023, 15:09:21) [GCC 13.2.1 20230801]
Benchmark script
from timeit import timeit
from statistics import mean, stdev
from operator import truth, not_
from collections import deque
from itertools import repeat
import sys
def Stefan1(factors):
product = iter([()])
for factor in factors:
product = (
p + (x,)
for product, (f, *args) in [(product, factor)]
for p in product
for x in f(*args)
)
return product
from functools import reduce
from operator import call
def Stefan2(factors):
return reduce(
lambda product, factor: (
p + (x,)
for p in product
for x in call(*factor)
),
factors,
iter([()])
)
from itertools import repeat, starmap, chain
from operator import call
def Stefan3(factors):
return chain.from_iterable(map(
zip,
map(repeat, call(*factors[0])),
starmap(call, repeat(factors[1]))
))
def Stefan4(factors):
iters = [iter(f(*args)) for f, *args in factors]
vals = list(map(next, iters))
while True:
yield tuple(vals)
for i, it in enumerate(reversed(iters)):
for vals[~i] in it:
break
else:
continue
break
else:
break
while i:
i -= 1
iters[~i] = iter(call(*factors[~i]))
vals[~i] = next(iters[~i])
def _gen(item):
f, *v = item
return f(*v)
def Karl(alist):
try:
first, *rest = alist
except ValueError: # not enough values to unpack
yield () # base case for zero input generators
else:
yield from (
(x, *y)
for x in _gen(first)
for y in Karl(rest)
)
def Karlish(alist):
if alist:
(f, *v), *rest = alist
yield from ((x, *y) for x in f(*v) for y in Karlish(rest))
else:
# Check sentinel
yield ()
def Karlish_no_wrap(alist):
if alist:
(f, *v), *rest = alist
for x in f(*v):
for y in Karlish_no_wrap(rest):
yield (x, *y)
else:
# Check sentinel
yield ()
funcs = Stefan1, Stefan2, Stefan3, Stefan4, Karl, Karlish, Karlish_no_wrap
alist = [(range, 300)] * 2
funcs = Stefan1, Stefan2, Stefan4, Karl, Karlish, Karlish_no_wrap
alist = [(range, 10)] * 5
times = {f: [] for f in funcs}
def stats(f):
ts = [t * 1e3 for t in sorted(times[f])[:5]]
return f'{mean(ts):6.2f} ± {stdev(ts):4.2f} ms '
for _ in range(25):
for f in funcs:
t = timeit(lambda: deque(f(alist), 0), number=1)
times[f].append(t)
for f in sorted(funcs, key=stats):
print(stats(f), f.__name__)
print('\nPython:', sys.version)
Attempt This Online!