import copy
from typing import Any, Union
from ._LooperParent import _LooperParent
[docs]class ListLooper(_LooperParent):
r"""Outputs slices of a :obj:`list` using the metaphor of a looping window
of a constant number of elements. This number is given by the argument
:attr:`window_size`, which is an :obj:`int` representing how many elements
are to be included in each slice.
For instance, if the initial container had the elements
``[A, B, C, D, E, F]`` (where each letter represents an element of an
arbitrary type) and the looping window was size ``3``, the output would be:
``[A B C] [B C D] [C D E] [D E F] [E F] [F]``
This can be better visualised as:
.. code-block:: none
A B C
B C D
C D E
D E F
E F
F
Basic usage:
Calling the object will return a :obj:`list` generated by the looping
process. Each call of the object will move the window forwards and
output the result.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper()
['A', 'B', 'C']
>>> looper()
['B', 'C', 'D']
The property :attr:`current_window` can be used to access the current
window without moving the head forwards.
>>> looper.current_window
['B', 'C', 'D']
:attr:`process_on_first_call`:
The very first call will output the input :obj:`list` without
processing it. To disable this behaviour and have the looping window
move on the very first call, initialise the class with the keyword
argument :attr:`process_on_first_call` set to ``True``.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list,
... window_size=3,
... process_on_first_call=True,
... )
>>> looper()
['B', 'C', 'D']
Using as iterator:
The instances of this class can also be used as an iterator, which can
then be used in a for loop to exhaust all windows.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list,
... window_size=3,
... )
>>> for window in looper:
... print(window)
['A', 'B', 'C']
['B', 'C', 'D']
['C', 'D', 'E']
['D', 'E', 'F']
['E', 'F']
['F']
Arguments and properties:
This class can take many optional keyword arguments during its
creation. :attr:`step_size` dictates the size of each individual step
in number of elements (default value is ``1``). :attr:`max_steps` sets
the maximum number of steps that the window can advance when the object
is called, ranging between ``1`` and the input value (default is also
``1``). :attr:`repetition_chance` sets the chance of a window result
repeating itself (that is, the window not moving forwards when called).
It should range from ``0.0`` to ``1.0`` (default ``0.0``, i.e. no
repetition). :attr:`forward_bias` sets the chance of the window moving
forward instead of backwards. It should range from ``0.0`` to ``1.0``
(default ``1.0``, which means the window can only move forwards. A
value of ``0.5`` gives 50% chance of moving forwards while a value of
``0.0`` will move the window only backwards). :attr:`head_position` can
be used to offset the starting position of the looping window. It must
be an :obj:`int` and its default value is ``0``. By default, calling
the object will first return the original container and subsequent
calls will process it; set :attr:`process_on_first_call` to ``True``
and the looping process will be applied on the very first call. Lastly,
set :attr:`end_with_max_n_elements` to ``True`` to end the process when
the final window has the maximum number of elements.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list,
... window_size=3,
... step_size=1,
... max_steps=2,
... repetition_chance=0.25,
... forward_bias=0.2,
... head_position=0,
... end_with_max_n_elements=True,
... process_on_first_call=True,
... )
>>> looper.window_size
3
>>> looper.step_size
1
>>> looper.repetition_chance
0.25
>>> looper.forward_bias
0.2
>>> looper.max_steps
2
>>> looper.head_position
0
>>> looper.end_with_max_n_elements
True
>>> looper.process_on_first_call
True
Use the properties below to change these values after initialisation.
>>> looper.window_size = 2
>>> looper.step_size = 2
>>> looper.max_steps = 3
>>> looper.repetition_chance = 0.1
>>> looper.forward_bias = 0.8
>>> looper.head_position = 2
>>> looper.end_with_max_n_elements = False
>>> looper.process_on_first_call = False
>>> looper.window_size
2
>>> looper.step_size
2
>>> looper.max_steps
3
>>> looper.repetition_chance
0.1
>>> looper.forward_bias
0.8
>>> looper.head_position
2
>>> looper.end_with_max_n_elements
False
>>> looper.process_on_first_call
False
Setting :attr:`forward_bias` to ``0.0``:
Set :attr:`forward_bias` to ``0.0`` to move backwards instead of
forwards (default is ``1.0``). The initial :attr:`head_position` must
be greater than ``0`` otherwise the contents will already be exhausted
in the very first call (since it will not be able to move backwards
from that position).
>>> input_list = ['A', 'B', 'C', 'D']
>>> looper = auxjad.ListLooper(input_list,
... window_size=2,
... head_position=2,
... forward_bias=0.0,
... )
>>> looper.output_all()
['C', 'D', 'B', 'C', 'A', 'B']
:attr:`forward_bias` between ``0.0`` and ``1.0``:
Setingt :attr:`forward_bias` to a value in between ``0.0`` and ``1.0``
will result in random steps being taken forward or backward, according
to the bias. The initial value of :attr:`head_position` will once gain
play an important role here, as the contents might be exhausted if the
looper attempts to move backwards after reaching the head position
``0``.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
>>> looper = auxjad.ListLooper(input_list,
... window_size=2,
... head_position=4,
... forward_bias=0.5,
... )
>>> looper.output_n(4)
['E', 'F', 'D', 'E', 'C', 'D', 'B', 'C']
:attr:`max_steps`:
Setting the keyword argument :attr:`max_steps` to a value larger than
``1`` will result in a random number of steps (between ``1`` and
:attr:`max_steps`) being applied at each call.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
>>> looper = auxjad.ListLooper(input_list,
... window_size=2,
... head_position=2,
... max_steps=4,
... )
>>> looper.output_n(4)
['C', 'D', 'D', 'E', 'E', 'F', 'H']
:func:`len()`:
The function :func:`len()` can be used to get the total number of
elements in the container.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> len(looper)
6
:meth:`output_all`:
To run through the whole process and output it as a single :obj:`list`,
from the initial head position until the process outputs the single
last element, use the method :meth:`output_all`.
>>> input_list = ['A', 'B', 'C', 'D']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper.output_all()
['A', 'B', 'C', 'B', 'C', 'D', 'C', 'D', 'D']
:meth:`output_n`:
To run through just part of the process and output it as a single
:obj:`list`, starting from the initial head position, use the method
:meth:`output_n` and pass the number of iterations as argument.
>>> input_list = ['A', 'B', 'C', 'D']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper.output_n(2)
['A', 'B', 'C', 'B', 'C', 'D']
:attr:`end_with_max_n_elements`:
When ``True``, the last bar in the output will contain the maximum
number of leaves given by :attr:`window_size`. E.g. consider the
elements ``[A, B, C, D]`` and the looping window was size ``3``;
setting :attr:`end_with_max_n_elements` to ``True`` will output:
``[A B C] [B C D]``
Setting it to ``False`` (which is this property's default value) will
produces:
``[A B C] [B C D] [C D] [D]``
Compare the two examples below:
>>> input_list = ['A', 'B', 'C', 'D']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper.output_all()
['A', 'B', 'C', 'B', 'C', 'D', 'C', 'D', 'D']
>>> input_list = ['A', 'B', 'C', 'D']
>>> looper = auxjad.ListLooper(input_list,
... window_size=3,
... end_with_max_n_elements=True,
... )
>>> looper.output_all()
['A', 'B', 'C', 'B', 'C', 'D']
:attr:`window_size`:
To change the size of the looping window after instantiation, use the
property :attr:`window_size`. In the example below, the initial window
is of size ``3``, and so the first call of the looper object outputs
the first, second, and third elements of the :obj:`list`. The window
size is then set to ``4``, and the looper is called again, moving to
the element in the next position, thus outputting the second, third,
fourth, and fifth elements.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper()
['A', 'B', 'C']
>>> looper.window_size = 4
>>> looper()
['B', 'C', 'D', 'E']
:attr:`contents`:
Use the :attr:`contents` property to read as well as overwrite the
contents of the looper. Notice that the :attr:`head_position` will
remain on its previous value and must be reset to ``0`` if that's
required.
>>> input_list = ['A', 'B', 'C', 'D', 'E', 'F']
>>> looper = auxjad.ListLooper(input_list,
... window_size=3,
... )
>>> looper.contents
['A', 'B', 'C', 'D', 'E', 'F']
>>> looper()
['A', 'B', 'C']
>>> looper()
['B', 'C', 'D']
>>> looper.contents = [0, 1, 2, 3, 4]
>>> looper.contents
[0, 1, 2, 3, 4]
>>> looper()
[1, 2, 3]
>>> looper.head_position = 0
>>> looper()
[0, 1, 2]
Types in the input list:
The input :obj:`list` can contain any types of elements:
>>> input_list = [123, 'foo', (3, 4), 3.14]
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> looper()
[123, 'foo', (3, 4)]
This also include Abjad's types. Abjad's exclusive membership
requirement is respected since each call returns a
:func:`copy.deepcopy` of the window. The same is true to the
:meth:`output_all` method.
>>> import abjad
>>> import copy
>>> input_list = [
... abjad.Container(r"c'4 d'4 e'4 f'4"),
... abjad.Container(r"fs'1"),
... abjad.Container(r"r2 bf4 c'4"),
... abjad.Container(r"c''2. r4"),
... ]
>>> looper = auxjad.ListLooper(input_list, window_size=3)
>>> staff = abjad.Staff()
>>> for element in looper.output_all():
... staff.append(element)
>>> abjad.show(staff)
.. docs::
\new Staff
{
{
c'4
d'4
e'4
f'4
}
{
fs'1
}
{
r2
bf4
c'4
}
{
fs'1
}
{
r2
bf4
c'4
}
{
c''2.
r4
}
{
r2
bf4
c'4
}
{
c''2.
r4
}
{
c''2.
r4
}
}
.. figure:: ../_images/ListLooper-kvxaoz53y5f.png
"""
### CLASS VARIABLES ###
__slots__ = ('_end_with_max_n_elements')
### INITIALISER ###
[docs] def __init__(self,
contents: list[Any],
*,
window_size: int,
step_size: int = 1,
max_steps: int = 1,
repetition_chance: float = 0.0,
forward_bias: float = 1.0,
head_position: int = 0,
end_with_max_n_elements: bool = False,
process_on_first_call: bool = False,
) -> None:
r'Initialises self.'
self.contents = contents
self.end_with_max_n_elements = end_with_max_n_elements
super().__init__(head_position=head_position,
window_size=window_size,
step_size=step_size,
max_steps=max_steps,
repetition_chance=repetition_chance,
forward_bias=forward_bias,
process_on_first_call=process_on_first_call,
)
### SPECIAL METHODS ###
[docs] def __repr__(self) -> str:
r'Returns interpreter representation of :attr:`contents`.'
return str(self._contents)
[docs] def __len__(self) -> int:
r'Returns a length of :attr:`contents`.'
return len(self._contents)
### PUBLIC METHODS ###
[docs] def output_all(self) -> list[Any]:
r"""Goes through the whole looping process and outputs a single
:obj:`list`. This method replaces the parent's one since the parent's
method outputs an |abjad.Selection|.
"""
dummy_container = []
while True:
try:
dummy_container.extend(self.__call__())
except RuntimeError:
break
return dummy_container[:]
[docs] def output_n(self, n: int) -> list[Any]:
r"""Goes through ``n`` iterations of the looping process and outputs a
single :obj:`list`. This method replaces the parent's one since the
parent's method outputs an |abjad.Selection|.
"""
if not isinstance(n, int):
raise TypeError("argument must be 'int'")
if n < 0:
raise ValueError("argument must be a positive 'int'")
dummy_container = []
for _ in range(n):
dummy_container.extend(self.__call__())
return dummy_container[:]
### PRIVATE METHODS ###
def _slice_contents(self) -> None:
r"""This method takes a slice with :attr:`window_size` number of
elements out of :attr:`contents` starting at the current
:attr:`head_position`.
"""
start = self._head_position
end = self._head_position + self._window_size
self._current_window = self._contents[start:end]
### PUBLIC PROPERTIES ###
@property
def contents(self) -> list[Any]:
r'The :obj:`list` to be sliced and looped.'
return self._contents
@contents.setter
def contents(self,
contents: list[Any],
) -> None:
if not isinstance(contents, list):
raise TypeError("'contents' must be 'list")
self._contents = contents[:]
self._is_first_window = True
@property
def current_window(self) -> Union[list[Any], None]:
r'Read-only property, returns the window at the current head position.'
if self._current_window is None:
return self._current_window
return copy.deepcopy(self._current_window)[:]
@property
def end_with_max_n_elements(self) -> bool:
r"""When ``True``, the last bar in the output will contain the maximum
number of elements given by :attr:`window_size`. E.g. consider the
elements ``[A, B, C, D]`` and the looping window was size ``3``;
setting :attr:`end_with_max_n_elements` to ``True`` will output:
``[A B C] [B C D]``
Setting it to ``False`` (which is this property's default value) will
produces:
``[A B C] [B C D] [C D] [D]``
"""
return self._end_with_max_n_elements
@end_with_max_n_elements.setter
def end_with_max_n_elements(self,
end_with_max_n_elements: bool,
) -> None:
if not isinstance(end_with_max_n_elements, bool):
raise TypeError("'end_with_max_n_elements' must be 'bool'")
self._end_with_max_n_elements = end_with_max_n_elements
### PRIVATE PROPERTIES ###
@property
def _done(self) -> bool:
r""":obj:`bool` indicating whether the process is done (i.e. whether
the head position has overtaken the :attr:`contents`'s length).
"""
if self._end_with_max_n_elements:
return (self._head_position + self._window_size > self.__len__()
or self._head_position < 0)
else:
return (self._head_position >= self.__len__()
or self._head_position < 0)