-3

I seek a plain Python function that accepts an arbitrary number of iterables (tuples, lists, dictionaries), and returns them shuffled in the same order:

a = (1, 2, {3: 4}, 5)
b = [(5,6), [7,8], [9,0], [1,2]]
c = {'arrow': 5, 'knee': 'guard', 0: ('x',2)}

x, y, z = magic(a, b, c)
print(x, y, z, sep='\n')
# ({3: 4}, 1, 2)
# [[9, 0], (5, 6), [7, 8]]
# {0: ('x', 2), 'arrow': 5, 'knee': 'guard'}

The function must:

  1. Return iterables shuffled in the same order (see above)
  2. Accept any number of iterables
  3. Preserve iterables types
  4. Support nested iterables of any depth and type
  5. Not shuffle nested elements themselves (eg. [7,8] above doesn't become [8,7])
  6. Return iterables with length of shortest iterable's length w/o raising error (see above)

OK if using Numpy, random, etc for the shuffle step (e.g. np.random.shuffle(magic_packing)), but cannot be a high-level library method (uses multiprocessing, encoding, etc - should be 'plain')


I've seen related SO's, but could not adapt them to such a generalized case. How can this be accomplished?
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
OverLordGoldDragon
  • 1
  • 9
  • 53
  • 101

3 Answers3

4

Here's a basic approach:

import random
def shuffle_containers(*args):
    min_length = min(map(len, args))
    idx = list(range(min_length))
    random.shuffle(idx)
    results = []
    for arg in args:
        if isinstance(arg, list):
            results.append([arg[i] for i in idx])
        elif isinstance(arg, tuple):
            results.append(tuple(arg[i] for i in idx))
        elif isinstance(arg, dict):
            items = list(arg.items())
            results.append(dict(items[i] for i in idx))
        else:
            raise ValueError(
                "Encountered", type(arg),
                "expecting only list, dict, or tuple"
            )
    return results
a = (1, 2, {3: 4}, 5)
b = [(5,6), [7,8], [9,0], [1,2]]
c = {'arrow': 5, 'knee': 'guard', 0: ('x',2)}
x, y, z = shuffle_containers(a, b, c)
print(x, y, z, sep='\n')

Note, this will ignore any items passed the length of the smallest container, if you don't want that, it will require more complicated logic.

EDIT:

Here it is in two lines of code:

def shuffle_containers(*args):
    min_length = min(map(len, args)); idx = list(range(min_length)); random.shuffle(idx)
    return [ [arg[i] for i in idx] if isinstance(arg, list) else tuple(arg[i] for i in idx) if isinstance(arg, tuple) else dict(list(args.items())[i] for i in idx) ]

Of course, the above is much less readble, less efficient, and less simple. Don't do stuff like that.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • 2
    @OverLordGoldDragon what would be "simpler", this is about as simple as it gets. In any case, how is this supposed to work if you want to maintain the order, say, `[1,2,3,4]` and `['a','b','c']`, what would you do if it didn't ignore `4` in the first list? – juanpa.arrivillaga Oct 01 '19 at 03:10
  • 8
    What does that have to do with anything? I could condense this into a horrible list-comprehension that would require 6 lines, but that wouldn't be "simpler" that would be "unreadble and unpythonic". – juanpa.arrivillaga Oct 01 '19 at 03:12
2
import random

a = (1, 2, {3: 4}, 5)
b = [(5,6), [7,8], [9,0], [1,2]]
c = {'arrow': 5, 'knee': 'guard', 0: ('x',2)}

def magic(*x):
    out = []
    # 6. length of shortest iterable
    min_len = min(len(a_org) for a_org in x)
    for a_org in x:
        if isinstance(a_org, list) or isinstance(a_org, tuple):
            indices = list(range(len(a_org)))
            random.shuffle(indices)
            a_copy = type(a_org)(a_org[i] for i in indices[:min_len])
        elif isinstance(a_org, dict):
            indices = list(a_org.keys())
            random.shuffle(indices)
            a_copy = {i:a_org[i] for i in indices[:min_len]}
        else:
            raise "not supported type"

        out.append(a_copy)
    return tuple(out)

print(magic(a, b, c))
Mehdi
  • 4,202
  • 5
  • 20
  • 36
-3
def ordered_shuffle(*args):
    args_types = [type(arg) for arg in args]                               # [1]
    _args      = [arg if type(arg)!=dict else arg.items() for arg in args] # [2]
    args_split = [arg for arg in zip(*_args)]                              # [3]
    args_shuffled = random.sample(args_split, len(args_split))             # [4]
    args_shuffled = map(tuple, zip(*args_shuffled))                        # [5]
    return [args_types[i](arg) for i, arg in enumerate(args_shuffled)]     # [6]

EXPLANATION: take a simpler case, step-by-step:

a = [1, 2, 3]
b = ([1,2], [3,4], [5,6])
c = {'a': 1, 'b': 2, 'c': 3}

# [1]: list, tuple, dict
# [2]: [[1, 2, 3],
#       ([1, 2], [3, 4], [5, 6]),
#       dict_items([('a', 1), ('b', 2), ('c', 3)])]
# [3]: [(1, [1, 2], ('a', 1)), 
#       (2, [3, 4], ('b', 2)), 
#       (3, [5, 6], ('c', 3))]
# [4]: [(1, [1, 2], ('a', 1)), 
#       (3, [5, 6], ('c', 3)), 
#       (2, [3, 4], ('b', 2))]
# [5]: (1, 2, 3)
#      ([1, 2], [3, 4], [5, 6])
#      (('a', 1), ('b', 2), ('c', 3))
# [6]: [(1, 2, {3: 4}),
#       [(5, 6), [7, 8], [9, 0]],
#       {'arrow': 5, 'knee': 'guard', 0: ('x', 2)}]
  1. Store original types to restore later
  2. By default, Python iterates only over dict KEYS - we need values also
  3. Python dictionaries cannot be shuffled directly - but indirectly by first casting key-value pairs into tuples; all iterables should also be iterated concurrently to restructure, accomplished w/ zip
  4. Use Python's native random
  5. Can cast into list also, but tuple is more efficient
  6. Restore original types and return as tuple to be unpacked as x, y = ordered_shuffle(a, b)


Lambda solution: (credit: Gloweye)
ordered_shuffle = lambda *args:[type(args[i])(arg) for i, arg in enumerate(map(tuple, 
            zip(*random.sample([y for y in zip(*[x if type(x)!=dict else x.items() 
            for x in args])], min(len(z) for z in args)))))]


Shortest & Fastest solution: (credit: GZ0) (shorten var names to make it shortest)
def ordered_shuffle(*args):
    zipped_args = list(zip(*(a.items() if isinstance(a, dict) else a for a in args)))
    random.shuffle(zipped_args)
    return [cls(elements) for cls, elements in zip(map(type, args), zip(*zipped_args))]
OverLordGoldDragon
  • 1
  • 9
  • 53
  • 101