Just for sports
a solution without explicit conditionals or predicates
(i.e., without any if
keywords):
from itertools import chain, repeat, permutations
from copy import deepcopy
def shuffle(*strings):
# Treat the strings as pools from which to draw elements in order.
# Convert the strings to lists, so that drawn items can be removed:
pools = (list(string) for string in strings)
# From each pool, we have to draw as many times as it has items:
pools_to_draw_from = chain.from_iterable(
repeat(pool, len(pool)) for pool in pools
)
# Because itertools.permutations treats elements as unique based on their
# position, not on their value and because pools_to_draw_from has repeated
# repeated items, we would get repeated permutations, if we would not
# filter them out with `unique`.
possible_drawing_orders = unique(permutations(pools_to_draw_from))
# For each drawing order, we want to draw (and thus remove) items from our
# pools. Subsequent draws within the same drawing order should get the
# respective next item in the pool, i.e., see the modified pool. But we don't
# want the pools to be exhausted after processing the first drawing ordering.
#
# Deepcopy preserves internal repetition and thus does exactly what we need.
possible_drawing_orders = (deepcopy(pdo) for pdo in possible_drawing_orders)
# Draw from the pools for each possible order,
# build strings and return them in a list:
return [''.join(_draw(p)) for p in possible_drawing_orders]
def _draw(drawing_order):
return (pool_to_draw_from.pop(0) for pool_to_draw_from in drawing_order)
We need a helper function for this:
from operator import itemgetter
from itertools import groupby
def unique(iterable, key=None):
# Other than unique_everseen from
# https://docs.python.org/3/library/itertools.html#itertools-recipes, this
# works for iterables of non-hashable elements, too.
return unique_justseen(sorted(iterable, key=key), key)
def unique_justseen(iterable, key=None):
"""
List unique elements, preserving order. Remember only the element just seen.
"""
# from https://docs.python.org/3/library/itertools.html#itertools-recipes
return map(next, map(itemgetter(1), groupby(iterable, key)))
If the number of non-unique permutations is large, this is probably rather inefficient, due to the call to sorted
. For alternatives to obtain unique permutations of non-unique values, see permutations with unique values.
TL;DR?
No problem. We can boil this approach down to this abomination:
from itertools import chain, repeat, permutations
from copy import deepcopy
def shuffle(*strings):
return list({''.join(l.pop(0) for l in deepcopy(p)) for p in permutations(chain.from_iterable(repeat(list(s), len(s)) for s in strings))})
(Using a set comprehension on the result instead of ensuring uniqueness earlier.)