简体   繁体   中英

Python - shuffle only some elements of a list

I'm trying to shuffle only elements of a list on 3rd till last position so the 1st two will always stay in place eg

list = ['a?','b','c','d','e']

into

list = ['a?','b','d','e','c']

and for some reason this doesn't work:

list = ['a?','b','c','d','e']
import random
random.shuffle(list[2:])    
print list

Any know what am I doing wrong??

The only thing that works for me is so far this (EDITED):

lists = [['a?','b','c','d','e'],['1?','2','3','4','5','6','7']]
import random

for list in lists:
    copy = list[2:]
    random.shuffle(copy)
    list[2:] = copy

print lists

Think this is exactly what I needed.

What you do is this:

copy = list[2:]
random.shuffle(copy)    

which does not do much to the original list. Try this:

copy = list[2:]
random.shuffle(copy)
list[2:] = copy # overwrite the original

If you want to shuffle without copying, you may try to write your own mutable slice class, like follows (that's a rough implementation sketch, no boundary checks etc):

class MutableSlice(object):
    def __init__(self, baselist, begin, end=None):
        self._base = baselist
        self._begin = begin
        self._end = len(baselist) if end is None else end

    def __len__(self):
        return self._end - self._begin

    def __getitem__(self, i):
        return self._base[self._begin + i]

    def __setitem__(self, i, val):
        self._base[i + self._begin] = val

Then wrap the original list into it and feed to the standard shuffle:

>>> mylist = [1,2,3,4,5,6]
>>> slice = MutableSlice(mylist, 2)
>>> import random
>>> random.shuffle(slice)
>>> mylist
[1, 2, 4, 3, 5, 6]

You can create your own shuffle function that will allow you to shuffle a slice within a mutable sequence. It handles sampling the slice copy and reassigning back to the slice. You must pass slice() arguments instead of the more familiar [2:] notation.

from random import sample
def myShuffle(x, *s):
    x[slice(*s)] = sample(x[slice(*s)], len(x[slice(*s)]))

usage:

>>> lst = ['a?','b','c','d','e']   #don't use list as a name
>>> myShuffle(lst, 2)              #shuffles lst[:2]
>>> lst
['b', 'a?', 'c', 'd', 'e']
>>> myShuffle(lst, 2, None)        #shuffles lst[2:]
>>> lst
['b', 'a?', 'd', 'e', 'c']

To shuffle a slice of the list in place, without copies, we can use a Knuth shuffle :

import random
def shuffle_slice(a, start, stop):
    i = start
    while (i < stop-1):
        idx = random.randrange(i, stop)
        a[i], a[idx] = a[idx], a[i]
        i += 1

It does the same thing as random.shuffle, except on a slice:

>>> a = [0, 1, 2, 3, 4, 5]
>>> shuffle_slice(a, 0, 3)
>>> a
[2, 0, 1, 3, 4, 5]

l[2:] constructs a new list, and random.shuffle tries to change the list "in-place," which has no effect on l itself.

You could use random.sample for this:

l[2:] = random.sample(l[2:], len(l)-2)

Using the fact that a list has fast remove and insert and exteding a previous solution ( https://stackoverflow.com/a/25229111/3449962 ):

List item

  • enumerate fixed elements and copy them and their index
  • delete fixed elements from list
  • shuffle remaining sub-set
  • put fixed elements back in

This will use in-place operations with memory overhead that depends on the number of fixed elements in the list. Linear in time. A possible more general implementation of shuffle_subset:

#!/usr/bin/env python
"""Shuffle elements in a list, except for a sub-set of the elments.

The sub-set are those elements that should retain their position in
the list.  Some example usage:

>>> from collections import namedtuple
>>> class CAnswer(namedtuple("CAnswer","x fixed")):
...             def __bool__(self):
...                     return self.fixed is True
...             __nonzero__ = __bool__  # For Python 2. Called by bool in Py2.
...             def __repr__(self):
...                     return "<CA: {}>".format(self.x)
...
>>> val = [3, 2, 0, 1, 5, 9, 4]
>>> fix = [2, 5]
>>> lst = [CAnswer(v, i in fix) for i, v in enumerate(val)]

>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]

Using a predicate to filter.

>>> for i in range(4):  # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, lambda x : x.fixed)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>]

>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst)           # predicate = bool()
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>] [<CA: 0>, <CA: 9>]

Exclude certain postions from the shuffle.  For example, exclude the
first two elements:

>>> fix = [0, 1]
>>> lst = [CAnswer(v, i in fix) for i, v in enumerate(val)]
>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, fix)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

Using a selector with the same number of elements as lst:

>>> fix = [0, 1]
>>> lst = [CAnswer(v, i in fix) for i, v in enumerate(val)]
>>> sel = [(i in fix) for i, _ in enumerate(val)]
>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     shuffle_subset(lst, sel)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

A generator as selector works fine too:

>>> fix = [0, 1]
>>> lst = [CAnswer(v, i in fix) for i, v in enumerate(val)]
>>> print("Start   ", 0, ": ", lst)
Start    0 :  [<CA: 3>, <CA: 2>, <CA: 0>, <CA: 1>, <CA: 5>, <CA: 9>, <CA: 4>]
>>> for i in range(4):                # doctest: +NORMALIZE_WHITESPACE
...     sel = ((i in fix) for i, _ in enumerate(val))
...     shuffle_subset(lst, sel)
...     print([lst[i] for i in fix], end=" ")
...
[<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>] [<CA: 3>, <CA: 2>]

"""
from __future__ import print_function
import random


def shuffle_subset(lst, predicate=None):
    """All elements in lst, except a sub-set, are shuffled.

    The predicate defines the sub-set of elements in lst that should
    not be shuffled:

      + The predicate is a callable that returns True for fixed
      elements, predicate(element) --> True or False.

      + If the predicate is None extract those elements where
      bool(element) == True.

      + The predicate is an iterable that is True for fixed elements
      or len(predicate) == len(lst).

      + The predicate is a list of indices of fixed elements in lst
      with len(predicate) < len(lst).

    """
    def extract_fixed_elements(pred, lst):
        try:
            if callable(pred) or pred is None:
                pred = bool if pred is None else pred
                fixed_subset = [(i, e) for i, e in enumerate(lst) if pred(e)]
            elif (hasattr(pred, '__next__') or len(pred) == len(lst)):
                fixed_subset = [(i, lst[i]) for i, p in enumerate(pred) if p]
            elif len(pred) < len(lst):
                fixed_subset = [(i, lst[i]) for i in pred]
            else:
                raise TypeError("Predicate {} not supported.".format(pred))
        except TypeError as err:
            raise TypeError("Predicate {} not supported. {}".format(pred, err))
        return fixed_subset
    #
    fixed_subset = extract_fixed_elements(predicate, lst)
    fixed_subset.reverse()      # Delete fixed elements from high index to low.
    for i, _ in fixed_subset:
        del lst[i]
    random.shuffle(lst)
    fixed_subset.reverse()      # Insert fixed elements from low index to high.
    for i, e in fixed_subset:
        lst.insert(i, e)


if __name__ == "__main__":
    import doctest
    doctest.testmod()

I copied the shuffle function from random.shuffle and adapted it, so that it will shuffle a list only on a defined range:

import random
a = range(0,20)
b = range(0,20)

def shuffle_slice(x, startIdx, endIdx):
    for i in reversed(xrange(startIdx+1, endIdx)):
       # pick an element in x[:i+1] with which to exchange x[i]
       j = random.randint(startIdx, i)
       x[i], x[j] = x[j], x[i]

#Shuffle from 5 until the end of a
shuffle_slice(a, 5, len(a))    
print a

#Shuffle b from 5 ... 15
shuffle_slice(b, 5, 15)
print b

The code above only shuffles the elements within the specified range. The shuffle is done inplace, ie no copy of the list is created.

Try this ..it's much simpler and does not make any copies of the list.
You can keep any of the elements fixed by just playing with the list indices.

working:

  1. create a new list of only the elements you want to shuffle.

  2. shuffle the new list.

  3. remove those elements you wanted to shuffle from your original list.

  4. insert the newly created list into the old list at the proper index

import random
    list = ['a?', 'b', 'c', 'd', 'e']

    v = []
    p = [v.append(list[c]) for c in range(2,len(list))] #step 1
    random.shuffle(v)  #step 2
    for c in range(2,len(list)):
        list.remove(list[c])  #step 3
        list.insert(c,v[c-2]) #step 4    #c-2 since the part to be shuffled begins from this index of list

    print(list)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM