Python - shuffle only some elements of a list

后端 未结 8 1482
礼貌的吻别
礼貌的吻别 2020-12-06 16:52

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

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


        
相关标签:
8条回答
  • 2020-12-06 17:35

    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()
    
    0 讨论(0)
  • 2020-12-06 17:39

    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
    
    0 讨论(0)
  • 2020-12-06 17:41

    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, i.e. no copy of the list is created.

    0 讨论(0)
  • 2020-12-06 17:46

    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)
    
    0 讨论(0)
  • 2020-12-06 17:47

    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]
    
    0 讨论(0)
  • 2020-12-06 17:51

    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]
    
    0 讨论(0)
提交回复
热议问题