Source code for whisker.random

#!/usr/bin/env python

"""
whisker/random.py

===============================================================================

    Copyright © 2011-2020 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of the Whisker Python client library.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

===============================================================================

**Randomization functions that may be used by Whisker tasks.**

"""

from itertools import islice
import logging
import operator
import random
from typing import Any, Callable, Generator, Iterable, List, Sequence

from cardinal_pythonlib.lists import flatten_list, sort_list_by_index_list
from cardinal_pythonlib.reprfunc import auto_repr

log = logging.getLogger(__name__)

SHUFFLE_FUNC_TYPE = Callable[[Sequence[Any]], List[int]]


# =============================================================================
# Basic list functions
# =============================================================================

[docs]def last_index_of(x: List[Any], value: Any) -> int: """ Gets the index of the last occurrence of ``value`` in the list ``x``. """ return len(x) - 1 - x[::-1].index(value)
[docs]def get_unique_values(iterable: Iterable[Any]) -> List[Any]: """ Gets the unique values of its input. See https://stackoverflow.com/questions/12897374/get-unique-values-from-a-list-in-python. (We don't use ``list(set(x))``, because if the elements of ``x`` are themselves lists (perfectly common!), that gives ``TypeError: unhashable type: 'list'``.) """ # noqa seen = set() result = [] for element in iterable: hashed = element if isinstance(element, dict): hashed = tuple(sorted(element.items())) elif isinstance(element, list): hashed = tuple(element) if hashed not in seen: result.append(element) seen.add(hashed) return result
[docs]def get_indexes_for_value(x: List[Any], value: Any) -> List[int]: """ Returns a list of indexes of ``x`` where its value is ``value``. """ return [i for i, item in enumerate(x) if item == value]
# ============================================================================= # Draw-without-replacement # =============================================================================
[docs]def make_dwor_hat(values: Iterable[Any], multiplier: int = 1) -> List[Any]: """ Makes a "hat" to draw values from. See :func:`gen_dwor`. Does not modify the starting list; returns a copy. """ assert multiplier >= 1, "Bad DWOR multiplier" hat = [] for v in values: for _ in range(multiplier): hat.append(v) random.shuffle(hat) # shuffle in place return hat
[docs]def gen_dwor(values: Iterable[Any], multiplier: int = 1) -> Generator[Any, None, None]: """ Generates values using a draw-without-replacement (DWOR) system. Args: values: values to generate multiplier: DWOR multiplier; see below. Yields: successive values Here's how it works. - Suppose ``values == [A, B, C]``. - We'll call :math:`n` the number of values (here, 3), and :math:`k` the "multiplier" parameter. - If you iterate through ``gen_dwor(values, multiplier=1)``, you will get a sequence that might look like this (with spaces added for clarity): .. code-block:: none CAB ABC BCA BAC BAC ACB CBA ... That is, individual are drawn randomly from a "hat" of size :math:`n = 3`, containing one of each thing from ``values``. When the hat is empty, it is refilled with :math:`n` more. - If you iterate through ``gen_dwor(values, multiplier=2)``, however, you might get this: .. code-block:: none AACBBC CABBAC BAACCB ... The computer has put :math:`k` copies of each value in the hat, and then draws one each time at random (so the hat starts with :math:`nk` values in it). When the hat is exhausted, it re-populates. The general idea is to provide randomness, but randomness that is constrained to prevent unlikely but awkward sequences like .. code-block:: none AAAAAAAAAAAAAAAA ... unlikely but possible with full randomness! yet also have the option to avoid predictability. With :math:`k = 1`, then a clever subject could infer exactly what's coming up on every :math:`n`\ th trial. So a low value of :math:`k` brings very few "runs" but some predictability; as :math:`k` approaches infinity, it's equivalent to full randomness; some reasonably low value of :math:`k` in between may be a useful experimental sweet spot. See also, for example: - http://www.whiskercontrol.com/help/FiveChoice/index.html?fivechoice_dwor.htm """ # noqa hat = [] while True: if not hat: hat = make_dwor_hat(values, multiplier) value = hat.pop() yield value
[docs]def get_dwor_list(values: Iterable[Any], length: int, multiplier: int = 1) -> List[Any]: """ Makes a fixed-length list via :func:`gen_dwor`. Args: values: values to pick from length: list length multiplier: DWOR multiplier Returns: list of length ``length`` Example: .. code-block:: python from whisker.random import get_dwor_list values = ["a", "b", "c"] print(get_dwor_list(values, length=24, multiplier=1)) print(get_dwor_list(values, length=24, multiplier=2)) print(get_dwor_list(values, length=24, multiplier=3)) """ return list(islice(gen_dwor(values, multiplier), length))
# ============================================================================= # Simple randomness # =============================================================================
[docs]def shuffle_list_slice(x: List[Any], start: int = None, end: int = None) -> None: """ Shuffles a segment of a list, ``x[start:end]``, in place. Note that ``start=None`` means "from the beginning" and ``end=None`` means "to the end". """ # log.debug("x={}, start={}, end={}".format(x, start, end)) copy = x[start:end] random.shuffle(copy) x[start:end] = copy
[docs]def shuffle_list_subset(x: List[Any], indexes: List[int]) -> None: """ Shuffles some elements of a list (in place). The elements to interchange (shuffle) as specified by ``indexes``. """ elements = [x[i] for i in indexes] random.shuffle(elements) for element_idx, x_idx in enumerate(indexes): x[x_idx] = elements[element_idx]
# ============================================================================= # Randomness within/across chunks # =============================================================================
[docs]def shuffle_list_within_chunks(x: List[Any], chunksize: int) -> None: """ Divides a list into chunks and shuffles WITHIN each chunk (in place). For example: .. code-block:: python x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] shuffle_list_within_chunks(x, 4) ``x`` might now be: .. code-block:: none [4, 1, 3, 2, 7, 5, 6, 8, 9, 12, 11, 10] ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^ """ starts = list(range(0, len(x), chunksize)) # noinspection PyTypeChecker ends = starts[1:] + [None] for start, end in zip(starts, ends): shuffle_list_slice(x, start, end)
[docs]def shuffle_list_chunks(x: List[Any], chunksize: int) -> None: """ Divides a list into chunks and shuffles the chunks themselves (in place). For example: .. code-block:: python x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] shuffle_list_chunks(x, 4) ``x`` might now be .. code-block:: none [5, 6, 7, 8, 1, 2, 3, 4, 9, 10, 11, 12] ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^ Uses :func:`cardinal_pythonlib.lists.flatten_list` and :func:`cardinal_pythonlib.lists.sort_list_by_index_list`. (I say that mainly to test Intersphinx, when it is enabled.) """ starts = list(range(0, len(x), chunksize)) ends = starts[1:] + [len(x)] # Shuffle the indexes rather than the array, then we can write back # in place. chunks = [] for start, end in zip(starts, ends): chunks.append(list(range(start, end))) random.shuffle(chunks) indexes = flatten_list(chunks) sort_list_by_index_list(x, indexes)
# ============================================================================= # Hierarchical randomness: early methods # =============================================================================
[docs]def block_shuffle_by_item(x: List[Any], indexorder: List[int], start: int = None, end: int = None) -> None: """ **DEPRECATED:** :func:`layered_shuffle` is more powerful. Shuffles the list ``x[start:end]`` hierarchically, in place. Args: x: list to shuffle indexorder: a list of indexes of each item of ``x`` The first index varies slowest; the last varies fastest. start: start index of ``x`` end: end index of ``x`` For example: .. code-block:: python p = list(itertools.product("ABC", "xyz", "123")) ``x`` is now a list of tuples looking like ``('A', 'x', '1')``. .. code-block:: python block_shuffle_by_item(p, [0, 1, 2]) ``p`` might now look like: .. code-block:: none C z 1 } all values of "123" appear } first "xyz" block C z 3 } once, but randomized } C z 2 } } } C y 2 } next "123" block } C y 1 } } C y 3 } } } C x 3 } C x 2 } C x 1 } A y 3 } second "xyz" block ... } ... A clearer explanation is in :func:`block_shuffle_by_attr`. """ item_idx_order = indexorder.copy() item_idx = item_idx_order.pop(0) # 1. Take copy sublist = x[start:end] # 2. Sort then shuffle in chunks (e.g. at the "ABC" level) sublist.sort(key=operator.itemgetter(item_idx)) # Note below that we must convert things to tuples to put them into # sets; if you take a set() of lists, you get # TypeError: unhashable type: 'list' unique_values = set(tuple(x[item_idx]) for x in sublist) chunks = [ [i for i, v in enumerate(sublist) if tuple(v[item_idx]) == value] for value in unique_values ] random.shuffle(chunks) indexes = flatten_list(chunks) sort_list_by_index_list(sublist, indexes) # 3. Call recursively (e.g. at the "xyz" level next) if item_idx_order: # more to do? starts_ends = [(min(chunk), max(chunk) + 1) for chunk in chunks] for s, e in starts_ends: block_shuffle_by_item(sublist, item_idx_order, s, e) # 4. Write back x[start:end] = sublist
[docs]def block_shuffle_by_attr(x: List[Any], attrorder: List[str], start: int = None, end: int = None) -> None: """ **DEPRECATED:** :func:`layered_shuffle` is more powerful. Exactly as for :func:`block_shuffle_by_item`, but by item attribute rather than item index number. For example: .. code-block:: python from collections import namedtuple import itertools from whisker.random import block_shuffle_by_attr p = list(itertools.product("ABC", "xyz", "123")) Trio = namedtuple("Trio", ["upper", "lower", "digit"]) q = [Trio(*x) for x in p] block_shuffle_by_attr(q, ['upper', 'lower', 'digit']) ``q`` started off as: .. code-block:: none [ Trio(upper='A', lower='x', digit='1'), Trio(upper='A', lower='x', digit='2'), Trio(upper='A', lower='x', digit='3'), Trio(upper='A', lower='y', digit='1'), Trio(upper='A', lower='y', digit='2'), Trio(upper='A', lower='y', digit='3'), Trio(upper='A', lower='z', digit='1'), Trio(upper='A', lower='z', digit='2'), Trio(upper='A', lower='z', digit='3'), Trio(upper='B', lower='x', digit='1'), Trio(upper='B', lower='x', digit='2'), Trio(upper='B', lower='x', digit='3'), Trio(upper='B', lower='y', digit='1'), Trio(upper='B', lower='y', digit='2'), Trio(upper='B', lower='y', digit='3'), Trio(upper='B', lower='z', digit='1'), Trio(upper='B', lower='z', digit='2'), Trio(upper='B', lower='z', digit='3'), Trio(upper='C', lower='x', digit='1'), Trio(upper='C', lower='x', digit='2'), Trio(upper='C', lower='x', digit='3'), Trio(upper='C', lower='y', digit='1'), Trio(upper='C', lower='y', digit='2'), Trio(upper='C', lower='y', digit='3'), Trio(upper='C', lower='z', digit='1'), Trio(upper='C', lower='z', digit='2'), Trio(upper='C', lower='z', digit='3') ] but after the shuffle ``q`` might now be: .. code-block:: none [ Trio(upper='B', lower='z', digit='1'), Trio(upper='B', lower='z', digit='3'), Trio(upper='B', lower='z', digit='2'), Trio(upper='B', lower='x', digit='1'), Trio(upper='B', lower='x', digit='3'), Trio(upper='B', lower='x', digit='2'), Trio(upper='B', lower='y', digit='3'), Trio(upper='B', lower='y', digit='2'), Trio(upper='B', lower='y', digit='1'), Trio(upper='A', lower='z', digit='2'), Trio(upper='A', lower='z', digit='1'), Trio(upper='A', lower='z', digit='3'), Trio(upper='A', lower='x', digit='1'), Trio(upper='A', lower='x', digit='2'), Trio(upper='A', lower='x', digit='3'), Trio(upper='A', lower='y', digit='3'), Trio(upper='A', lower='y', digit='1'), Trio(upper='A', lower='y', digit='2'), Trio(upper='C', lower='x', digit='2'), Trio(upper='C', lower='x', digit='3'), Trio(upper='C', lower='x', digit='1'), Trio(upper='C', lower='y', digit='2'), Trio(upper='C', lower='y', digit='1'), Trio(upper='C', lower='y', digit='3'), Trio(upper='C', lower='z', digit='1'), Trio(upper='C', lower='z', digit='2'), Trio(upper='C', lower='z', digit='3') ] You can see that the ``A``/``B``/``C`` group has been shuffled as blocks. Then, within ``B``, the ``x``/``y``/``z`` groups have been shuffled (and so on for ``A`` and ``C``). Then, within ``B.z``, the ``1``/``2``/``3`` values have been shuffled (and so on). """ item_attr_order = attrorder.copy() item_attr = item_attr_order.pop(0) # 1. Take copy sublist = x[start:end] # 2. Sort then shuffle in chunks sublist.sort(key=operator.attrgetter(item_attr)) unique_values = set(tuple(getattr(x, item_attr)) for x in sublist) chunks = [ [ i for i, v in enumerate(sublist) if tuple(getattr(v, item_attr)) == value ] for value in unique_values ] random.shuffle(chunks) indexes = flatten_list(chunks) sort_list_by_index_list(sublist, indexes) # 3. Call recursively (e.g. at the "xyz" level next) if item_attr_order: # more to do? starts_ends = [(min(chunk), max(chunk) + 1) for chunk in chunks] for s, e in starts_ends: block_shuffle_by_attr(sublist, item_attr_order, s, e) # 4. Write back x[start:end] = sublist
[docs]def shuffle_where_equal_by_attr(x: List[Any], attrname: str) -> None: """ **DEPRECATED:** :func:`layered_shuffle` is more powerful. Shuffles a list ``x``, in place, where list members are equal as judged by the attribute ``attrname``. This is easiest to show by example: .. code-block:: python from collections import namedtuple import itertools from whisker.random import shuffle_where_equal_by_attr p = list(itertools.product("ABC", "xyz", "123")) Trio = namedtuple("Trio", ["upper", "lower", "digit"]) q = [Trio(*x) for x in p] shuffle_where_equal_by_attr(q, 'digit') ``q`` started off as: .. code-block:: none [ Trio(upper='A', lower='x', digit='1'), Trio(upper='A', lower='x', digit='2'), Trio(upper='A', lower='x', digit='3'), Trio(upper='A', lower='y', digit='1'), Trio(upper='A', lower='y', digit='2'), Trio(upper='A', lower='y', digit='3'), Trio(upper='A', lower='z', digit='1'), Trio(upper='A', lower='z', digit='2'), Trio(upper='A', lower='z', digit='3'), Trio(upper='B', lower='x', digit='1'), Trio(upper='B', lower='x', digit='2'), Trio(upper='B', lower='x', digit='3'), Trio(upper='B', lower='y', digit='1'), Trio(upper='B', lower='y', digit='2'), Trio(upper='B', lower='y', digit='3'), Trio(upper='B', lower='z', digit='1'), Trio(upper='B', lower='z', digit='2'), Trio(upper='B', lower='z', digit='3'), Trio(upper='C', lower='x', digit='1'), Trio(upper='C', lower='x', digit='2'), Trio(upper='C', lower='x', digit='3'), Trio(upper='C', lower='y', digit='1'), Trio(upper='C', lower='y', digit='2'), Trio(upper='C', lower='y', digit='3'), Trio(upper='C', lower='z', digit='1'), Trio(upper='C', lower='z', digit='2'), Trio(upper='C', lower='z', digit='3') ] but after the shuffle ``q`` might now be: .. code-block:: none [ Trio(upper='A', lower='x', digit='1'), Trio(upper='A', lower='y', digit='2'), Trio(upper='A', lower='z', digit='3'), Trio(upper='B', lower='z', digit='1'), Trio(upper='A', lower='z', digit='2'), Trio(upper='C', lower='x', digit='3'), Trio(upper='B', lower='y', digit='1'), Trio(upper='A', lower='x', digit='2'), Trio(upper='C', lower='y', digit='3'), Trio(upper='A', lower='y', digit='1'), Trio(upper='C', lower='y', digit='2'), Trio(upper='C', lower='z', digit='3'), Trio(upper='C', lower='y', digit='1'), Trio(upper='C', lower='z', digit='2'), Trio(upper='A', lower='y', digit='3'), Trio(upper='B', lower='x', digit='1'), Trio(upper='B', lower='z', digit='2'), Trio(upper='B', lower='y', digit='3'), Trio(upper='C', lower='z', digit='1'), Trio(upper='C', lower='x', digit='2'), Trio(upper='B', lower='z', digit='3'), Trio(upper='C', lower='x', digit='1'), Trio(upper='B', lower='x', digit='2'), Trio(upper='A', lower='x', digit='3'), Trio(upper='A', lower='z', digit='1'), Trio(upper='B', lower='y', digit='2'), Trio(upper='B', lower='x', digit='3') ] As you can see, the ``digit`` attribute seems to have stayed frozen and everything else has jumbled. What has actually happened is that everything with ``digit == 1`` has been shuffled among themselves, and similarly for ``digit == 2`` and ``digit == 3``. """ unique_values = set(tuple(getattr(item, attrname)) for item in x) for value in unique_values: indexes = [ i for i, item in enumerate(x) if tuple(getattr(item, attrname)) == value ] shuffle_list_subset(x, indexes)
# ============================================================================= # Hierarchical randomness: full power # =============================================================================
[docs]def random_shuffle_indexes(x: List[Any]) -> List[int]: """ Returns a list of indexes of ``x``, randomly shuffled. """ indexes = list(range(len(x))) random.shuffle(indexes) # in place return indexes
[docs]def block_shuffle_indexes_by_value(x: List[Any]) -> List[int]: """ Returns a list of indexes of ``x``, block-shuffled by value. That is: we aggregate items into blocks, defined by value, and shuffle those blocks, returning the corresponding indexes of the original list. """ unique_values = get_unique_values(x) list_of_chunks = [ get_indexes_for_value(x, value) for value in unique_values ] random.shuffle(list_of_chunks) indexes = flatten_list(list_of_chunks) return indexes
[docs]def dwor_shuffle_indexes(x: List[Any], multiplier: int = 1) -> List[int]: """ Returns a list of indexes of ``x``, DWOR-shuffled by value. This is a bit tricky as we don't have a guarantee of equal numbers. It does sensible things in those circumstances. """ assert multiplier >= 1, "Bad DWOR multiplier" unique_values = get_unique_values(x) list_of_chunks = [ get_indexes_for_value(x, value) for value in unique_values ] def unusual_make_dwor_hat() -> List[int]: hat_ = [] for _ in range(multiplier): for chunk in list_of_chunks: if chunk: hat_.append(chunk.pop(0)) random.shuffle(hat_) # in place return hat_ hat = [] indexes = [] # type: List[int] finished = False while not finished: if not hat: hat = unusual_make_dwor_hat() if hat: indexes.append(hat.pop()) else: finished = True return indexes
[docs]def sort_indexes(x: List[Any]) -> List[int]: """ Returns the indexes of ``x`` in an order that would sort ``x`` by value. See https://stackoverflow.com/questions/7851077/how-to-return-index-of-a-sorted-list """ # noqa return sorted(range(len(x)), key=lambda index: x[index])
[docs]def reverse_sort_indexes(x: List[Any]) -> List[int]: """ Returns the indexes of ``x`` in an order that would reverse-sort ``x`` by value. """ return sorted(range(len(x)), key=lambda index: x[index], reverse=True)
[docs]class ShuffleLayerMethod(object): """ Class to representing instructions to :func:`layered_shuffle` (q.v.). """ def __init__(self, flat: bool = False, layer_key: int = None, layer_attr: str = None, layer_func: Callable[[Any], Any] = None, shuffle_func: SHUFFLE_FUNC_TYPE = None) -> None: """ Args: flat: take data as ``x[index]``? layer_key: take data as ``x[index][layer_key]``? layer_attr: take data as ``getattr(x[index], layer_attr)``? layer_func: take data as ``layer_func(x[index])``? shuffle_func: function (N.B. may be a lambda function with parameters attached) that takes a list of objects and returns a list of INDEXES, suitably shuffled. Typical values of ``shuffle_func``: - ``None``: no shuffle - ``random_shuffle_indexes``: plain shuffle; see :func:`random_shuffle_indexes` - ``dwor_shuffle_indexes``: DWOR-style shuffle (will need a ``lambda`` for its ``multiplier parameter``; see :func:`dwor_shuffle_indexes` - ``block_shuffle_indexes_by_value``: aggregate items into blocks, defined by value, and shuffle those blocks; see :func:`block_shuffle_indexes_by_value` - ``sort_indexes``: not exactly shuffling! See :func:`sort_indexes`. - ``reverse_sort_indexes``: not exactly shuffling! See :func:`reverse_sort_indexes`. """ assert ( flat + (layer_key is not None) + bool(layer_attr) ) == 1, "Specify exactly one way of accessing data from the layer!" if layer_key is not None: assert isinstance(layer_key, int) assert layer_key > 0 elif layer_attr: assert isinstance(layer_attr, str) elif layer_func: assert callable(layer_func) self.flat = flat self.layer_key = layer_key self.layer_attr = layer_attr self.layer_func = layer_func self.shuffle_func = shuffle_func def __repr__(self) -> str: return auto_repr(self)
[docs] def get_values(self, x: List[Any]) -> List[Any]: """ Returns all the values of interest from ``x`` for this layer. """ if self.flat: return x[:] # a copy, just in case someone wants to modify it elif self.layer_attr: attr = self.layer_attr return [getattr(item, attr) for item in x] elif self.layer_func: func = self.layer_func return [func(item) for item in x] else: # key, via item[key], meaning item.__getitem__(key) key = self.layer_key return [item[key] for item in x]
[docs] def get_unique_values(self, x: List[Any]) -> List[Any]: """ Returns all the unique values of ``x`` for this layer. """ return get_unique_values(self.get_values(x))
[docs] def get_indexes_for_value(self, x: List[Any], value: Any) -> List[int]: """ Returns a list of indexes of ``x`` where its value (as defined by this layer) is ``value``. """ return get_indexes_for_value(self.get_values(x), value)
[docs]def layered_shuffle(x: List[Any], layers: List[ShuffleLayerMethod]) -> None: r""" Most powerful hierarchical shuffle command here. Shuffles ``x`` in place in a layered way as specified by the sequence of methods. In more detail: - for each layer, it shuffles values of ``x`` as defined by the :class:`ShuffleLayerMethod` (for example: "shuffle ``x`` in blocks based on the value of ``x.someattr``", or "shuffle ``x`` randomly") - it then proceeds to deeper layers *within* sub-lists defined by each unique value from the previous layer. Args: x: sequence (e.g. list) to shuffle layers: list of :class:`ShuffleLayerMethod` instructions Examples: .. code-block:: python from collections import namedtuple import itertools import logging import random from whisker.random import * logging.basicConfig(level=logging.DEBUG) startlist = ["a", "b", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d"] x1 = startlist[:] x2 = startlist[:] x3 = startlist[:] x4 = startlist[:] do_nothing_method = ShuffleLayerMethod(flat=True, shuffle_func=None) do_nothing_method.get_unique_values(x1) do_nothing_method.get_indexes_for_value(x1, "b") layered_shuffle(x1, [do_nothing_method]) print(x1) flat_randomshuffle_method = ShuffleLayerMethod( flat=True, shuffle_func=random_shuffle_indexes) flat_randomshuffle_method.get_unique_values(x1) flat_randomshuffle_method.get_indexes_for_value(x1, "b") layered_shuffle(x1, [flat_randomshuffle_method]) print(x1) flat_blockshuffle_method = ShuffleLayerMethod( flat=True, shuffle_func=block_shuffle_indexes_by_value) layered_shuffle(x2, [flat_blockshuffle_method]) print(x2) flat_dworshuffle_method = ShuffleLayerMethod( flat=True, shuffle_func=dwor_shuffle_indexes) layered_shuffle(x3, [flat_dworshuffle_method]) print(x3) flat_dworshuffle2_method = ShuffleLayerMethod( flat=True, shuffle_func=lambda x: dwor_shuffle_indexes(x, multiplier=2)) layered_shuffle(x4, [flat_dworshuffle2_method]) print(x4) p = list(itertools.product("ABC", "xyz", "123")) Trio = namedtuple("Trio", ["upper", "lower", "digit"]) q = [Trio(*x) for x in p] print("\n".join(str(x) for x in q)) upper_method = ShuffleLayerMethod( layer_attr="upper", shuffle_func=block_shuffle_indexes_by_value) lower_method = ShuffleLayerMethod( layer_attr="lower", shuffle_func=reverse_sort_indexes) digit_method = ShuffleLayerMethod( layer_attr="digit", shuffle_func=random_shuffle_indexes) layered_shuffle(q, [upper_method, lower_method, digit_method]) print("\n".join(str(x) for x in q)) """ # noqa if not layers: return layer = layers[0] subsequent_layers = layers[1:] if layer.shuffle_func: values = layer.get_values(x) shuffled_indexes = layer.shuffle_func(values) log.debug("Applying function {!r} to values {!r} gives indexes " "{!r}".format(layer.shuffle_func, values, shuffled_indexes)) sort_list_by_index_list(x, shuffled_indexes) if subsequent_layers: unique_values = layer.get_unique_values(x) for value in unique_values: indexes_for_value = layer.get_indexes_for_value(x, value) subelements = [x[i] for i in indexes_for_value] # Recursion: layered_shuffle(subelements, subsequent_layers) # Put the (shuffled( elements back: for element_idx, x_idx in enumerate(indexes_for_value): x[x_idx] = subelements[element_idx]