def block_shuffle_by_attr(x: List[Any],
                          attrorder: List[str],
                          start: int = None,
                          end: int = None) -> None:
    """
    Exactly as for block_shuffle_by_item, but by item attribute
    rather than item index number.

    For example:

        p = list(itertools.product("ABC", "xyz", "123"))
        q = [AttrDict({'one': x[0], 'two': x[1], 'three': x[2]}) for x in p]
        block_shuffle_by_attr(q, ['one', 'two', 'three'])
    """
    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
def shuffle_list_chunks(x: List[Any], chunksize: int) -> None:
    """
    Divides a list into chunks and shuffles the chunks themselves (in place).
    For example:
        x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
        shuffle_list_chunks(x, 4)
    x might now be [5, 6, 7, 8, 1, 2, 3, 4, 9, 10, 11, 12]
                    ^^^^^^^^^^  ^^^^^^^^^^  ^^^^^^^^^^^^^
    """
    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)
def shuffle_list_chunks(x: List[Any], chunksize: int) -> None:
    """
    Divides a list into chunks and shuffles the chunks themselves (in place).
    For example:
        x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
        shuffle_list_chunks(x, 4)
    x might now be [5, 6, 7, 8, 1, 2, 3, 4, 9, 10, 11, 12]
                    ^^^^^^^^^^  ^^^^^^^^^^  ^^^^^^^^^^^^^
    """
    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)
def block_shuffle_by_attr(x: List[Any],
                          attrorder: List[str],
                          start: int = None,
                          end: int = None) -> None:
    """
    Exactly as for block_shuffle_by_item, but by item attribute
    rather than item index number.

    For example:

        p = list(itertools.product("ABC", "xyz", "123"))
        q = [AttrDict({'one': x[0], 'two': x[1], 'three': x[2]}) for x in p]
        block_shuffle_by_attr(q, ['one', 'two', 'three'])
    """
    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
def block_shuffle_by_item(x: List[Any],
                          indexorder: List[int],
                          start: int = None,
                          end: int = None) -> None:
    """
    Shuffles the list x hierarchically, in place.
    indexorder is a list of indexes of each item of x.
    The first index varies slowest; the last varies fastest.

    For example:

        p = list(itertools.product("ABC", "xyz", "123"))

    x is now a list of tuples looking like ('A', 'x', '1')

        block_shuffle_by_item(p, [0, 1, 2])

    p might now look like:

        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
        ...                                 } ...

    """
    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
def block_shuffle_by_item(x: List[Any],
                          indexorder: List[int],
                          start: int = None,
                          end: int = None) -> None:
    """
    Shuffles the list x hierarchically, in place.
    indexorder is a list of indexes of each item of x.
    The first index varies slowest; the last varies fastest.

    For example:

        p = list(itertools.product("ABC", "xyz", "123"))

    x is now a list of tuples looking like ('A', 'x', '1')

        block_shuffle_by_item(p, [0, 1, 2])

    p might now look like:

        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
        ...                                 } ...

    """
    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