Shuffling specific element within a nested list of lists

Viewed 155

I'm trying to randomly shuffle one element within a nested list of lists.

E.g. I have:

list1=[[['a', 'b', 'c', 1], ['a', 'b', 'c', 2]], [['a', 'b', 'c', 3], ['a', 'b', 'c', 4]], [['a', 'b', 'c', 5], ['a', 'b', 'c', 6]]]

I want to randomly shuffle the 3rd element of each sub-list (the float) while keeping the rest of the list entries and their structure within this nested list intact. E.g. as such:

list1=[[['a', 'b', 'c', 1], ['a', 'b', 'c', 6]], [['a', 'b', 'c', 4], ['a', 'b', 'c', 2]], [['a', 'b', 'c', 3], ['a', 'b', 'c', 5]]]

So far, I've come up with the following:

import random

list1 = [[['a', 'b', 'c', 1], ['a', 'b', 'c', 2]], [['a', 'b', 'c', 3], ['a', 'b', 'c', 4]], [['a', 'b', 'c', 5], ['a', 'b', 'c', 6]]]

vals = [x[3] for line in list1 for x in line]
shuffled_vals = random.sample(vals,len(vals))

counter = 0
for i in range(len(list1)):
    for j in range(len(list1[i])):
        list1[i][j][3] = shuffled_vals[counter]
        counter += 1

Although this works as intendend, I'd be curious if there is a more elegant / Pythonic solution. Also, I'm not sure how well this scales - the list I intend to use it on will contain several million entries.

Any tips for improving this code (to a more Pythonic or more efficient one) are greatly appreciated.

2 Answers

Your code looks good to me. I would do almost the same except I wouldn't mutate (modify in-place) the input list. Usually you would also avoid using range(len(X)) and prefer iterating over the elements of X directly.

Putting this together that would give something like this:

from random import sample

vals = [x[3] for line in list1 for x in line]
vals = sample(vals, len(vals))

new_list = [[sub_l[:3] + [vals.pop()] for sub_l in l] for l in list1]

Here I also make use of .pop() because we can throw the vals away as soon as we've used them. Which save me from using something like the counter you had.

Maybe a small improvement would be to use mutation where we have perfect control over the object. Using random.sample() is quite handy if you want to avoid mutation, but if you don't care you can simply replace vals = sample(vals, len(vals)) by random.shuffle(vals). Which I feel is a bit better here since it's more readable.

Using an iterator might be helpful:

import random
list1=[[['a', 'b', 'c', 1], ['a', 'b', 'c', 2]], [['a', 'b', 'c', 3], ['a', 'b', 'c', 4]], [['a', 'b', 'c', 5], ['a', 'b', 'c', 6]]]
mix = iter(random.sample([i[3] for j in list1 for i in j], len([i[3] for j in list1 for i in j])))
for i, v in enumerate(list1):
    for j, w in enumerate(v):
        list1[i][j][3] = next(mix)
print(list1)
[[['a', 'b', 'c', 5], ['a', 'b', 'c', 6]], [['a', 'b', 'c', 3], ['a', 'b', 'c', 2]], [['a', 'b', 'c', 1], ['a', 'b', 'c', 4]]]

Or as a function:

def shuffle_third(input_list):
    current_third = [i[3] for j in input_list for i in j]
    mix = iter(random.sample(current_third, len(current_third)))
    for i, v in enumerate(input_list):
        for j, w in enumerate(v):
            input_list[i][j][3] = next(mix)
    return input_list

I didn't manage to get a recusive replacement for the list but here is a recursive function that will dive down until it finds a single list and returns all elements in a specified position:

def any_pos_recursive(input_list, pos=0):
    def recurse(next_layer, insert=False):
        if type(next_layer[0]) is list:
            for i in next_layer: recurse(i)
        elif not insert: shuffle_list.append(next_layer[pos])
    shuffle_list = []
    recurse(input_list)
    shuffle_iter = iter(random.sample(shuffle_list, len(shuffle_list)))
    print(shuffle_list)

any_pos_recursive(list1)
Related