Source code for auxjad.core.ListLooper

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)