Source code for blueprints.utils.view

"""
If the children attributes of a :class:`NodeThing <blueprints.thing.node.NodeThing>` are called, those children will be 
returned in a :class:`View`. The :class:`View` can be used to get and set attributes of multiple objects 
as well as to call methods on those child Things simultaneously. The following example shows how to build 
a simplified hand model using Views.

.. code-block:: python
	:caption: Getting attributes
		
	>>> names = ['thumb', 'index', 'middle', 'ring', 'pinky']
	>>> fingers = [blue.Body(name=name) for name in names]
	>>> palm = blue.Body(name='palm', bodies=fingers)
	>>> view = palm.bodies # calling child attributes creates a view
	>>> view
	View[5|palm.bodies]
	>>> view.name
	['thumb', 'index', 'middle', 'ring', 'pinky']
	>>> view.bodies
	View[0|palm.bodies.bodies]
	>>> intermediate = blue.Body(name='intermediate')
	>>> distal = blue.Body(name='distal')
	>>> phalanges = [distal, intermediate, intermediate, intermediate, intermediate]
	>>> view.attach.distribute(phalanges) 
	>>> view.bodies[1:].attach(blue.Body(name='distal'))

.. code-block:: python
	:caption: Setting attributes
		
	>>> view.bodies[0].name = 'distal_thumb'
	>>> view.bodies[1:].name = ['intermediate_index', 
				    'intermediate_middle', 
				    'intermediate_ring', 
				    'intermediate_pinky']
	>>> view.bodies.name
	['distal_thumb', 
	 'intermediate_index', 
	 'intermediate_middle',
	 'intermediate_ring', 
	 'intermediate_pinky']

.. code-block::
	:caption: Applying functions
		
	>>> renaming = lambda x: x.__setattr__('name', f'distal_{x.parent.parent.name}')
	>>> view.bodies.bodies.apply(renaming)
	>>> view.bodies.bodies.name
	['distal_index', 'distal_middle', 'distal_ring', 'distal_pinky']

.. code-block:: mxml
	:caption: XML structure

	<body name="palm">
	    <body name="thumb">
	        <body name="distal_thumb" />
	    </body>
	    <body name="index">
	        <body name="intermediate_index">
	            <body name="distal_index" />
	        </body>
	    </body>
	    <body name="middle">
	        <body name="intermediate_middle">
	            <body name="distal_middle" />
	        </body>
	    </body>
	    <body name="ring">
	        <body name="intermediate_ring">
	            <body name="distal_ring" />
	        </body>
	    </body>
	    <body name="pinky">
	        <body name="intermediate_pinky">
	            <body name="distal_pinky" />
	        </body>
	    </body>
	</body>
"""

import numpy as np
import blueprints as blue
from collections import defaultdict, deque
from itertools import count, chain
from types import BuiltinFunctionType, FunctionType, MethodType



[docs] class View(blue.ViewType): """ The View class is used to handle the access to children of nodes in a kinematic tree and their properties. """ __RESERVED = {'_View__ELEMENTS', '_View__NAME', '_View__PARENT', '_View__RESERVED', '_AllView__ELEMENTS', '_AllView__NAME', '_AllView__PARENT', '_AllView__RESERVED', '_AllView__DESCENDANTS'}
[docs] @blue.restrict def __init__(self, elements: list, name: str, parent: blue.ThingType|blue.LatticeType) -> None: """ The View can be used to get and set attributes of multiple objects at the same time. Parameters ---------- elements : list All Things, that are part of the View. name : str The name of the attribute that was requested for this View. parent : blue.ThingType The parent of the View. """ # ELIMINATE DUPLICATES (order-preserving, O(n)) elements = list(dict.fromkeys(elements)) self.__ELEMENTS = elements.copy() self.__NAME = name self.__PARENT = parent
def __repr__(self) -> str: """ Represents the View as a string. Returns ------- str The representing string contains a representation of the call that was used to get the View. """ return f'View[{len(self)}|{self.__PARENT.name}.{self.__NAME}]' def __len__(self): """ The number of Things contained in the View. Returns ------- int The number of Things contained in the View. """ return len(self.__ELEMENTS) def __iter__(self): """ Used to iterate over all Things in the View. While iterating over the View, its contents should not be modified. Yields ------ ThingType The next Thing in the list of elements. """ for element in self.__ELEMENTS: yield element def __bool__(self): """ Indicating whether the View contains elements. Returns ------- bool If the View is empty the resulting value is False and True otherwise. """ return len(self.__ELEMENTS) > 0 @blue.restrict def __getitem__(self, key: int|slice|str) -> blue.ThingType|blue.ViewType: """ This method either returns a single Thing from the View or a new View from a subset of the View. Parameters ---------- key : int | slice | str If the argument is an integer, the Thing with its corresponding index is returned. If the argument is a slice, a View containing the Things according to the slices indicies is returned. if the argument is a string, a View containing the Things with the same user defined names is returned. Returns ------- blue.ThingType | blue.ViewType Raises ------ NotImplemented If the argument is neither (int, slice, str) NotImplemented is raised. """ if isinstance(key, int): return self.__ELEMENTS[key] elif isinstance(key, slice): return View(self.__ELEMENTS[key], self.__NAME, self.__PARENT) elif isinstance(key, str): return View(list(filter(lambda x: x._name == key, self)), self.__NAME, self.__PARENT) else: raise NotImplemented def __getattr__(self, attr: str) -> list|blue.ViewType|blue.FunctionHandleType: """ This methods handles the access to the attributes of the Things contained in the View depending on the type of attribute: 1. *property* In this case a list of all the singularly retrieved attributes is returned. 2. *children* This returns a new View, that contains the children of all the Views Things. 3. *method/function* In this case a FunctionHandle is returned. In the last case the :class:`FunctionHandle` can either be called directly and all arguments are passed to each of the Views Thing or the distribute method of the FunctionHandle can be used, to match each argument to each Thing in a one to one relation. .. code-block:: :caption: Example >>> names = ['thumb', 'index', 'middle', 'ring', 'pinky'] >>> fingers = [blue.Body(name=name) for name in names] >>> palm = blue.Body(name='palm', bodies=fingers) >>> view = palm.bodies # calling child attributes creates a view >>> view View[5|palm.bodies] >>> view.name # CASE 1 ['thumb', 'index', 'middle', 'ring', 'pinky'] >>> view.bodies # CASE 2 View[0|palm.bodies.bodies] >>> intermediate = blue.Body(name='intermediate') >>> distal = blue.Body(name='distal') >>> phalanges = [distal, intermediate, intermediate, intermediate, intermediate] >>> view.attach.distribute(phalanges) # CASE 3 >>> view.bodies[1:].attach(blue.Body(name='distal')) # CASE 3 .. code-block:: mxml :caption: XML structure of the previous example <body name="palm"> <body name="thumb"> <body name="distal_(0)" /> </body> <body name="index"> <body name="intermediate_(0)"> <body name="distal_(1)" /> </body> </body> <body name="middle"> <body name="intermediate_(1)"> <body name="distal_(2)" /> </body> </body> <body name="ring"> <body name="intermediate_(2)"> <body name="distal_(3)" /> </body> </body> <body name="pinky"> <body name="intermediate_(3)"> <body name="distal_(4)" /> </body> </body> </body> Parameters ---------- attr : str All attributes are valid, aslong as they are properties or methods of the Thing type contained in the View. Returns ------- list | blue.ViewType | blue.FunctionHandleType Raises ------ AttributeError If the attribute cannot be retrieved an Error is raised. """ if len(self) == 0: raise AttributeError(f'{self} is empty, attribute "{attr}" cannot be retrieved.') if all(hasattr(x, attr) for x in self): elements = list(map(lambda x: getattr(x, attr), self)) if all(map(lambda x: isinstance(x, blue.ViewType), elements)): elements = list((item for view in elements for item in view)) return View(elements, f'{self.__NAME}.{attr}', self.__PARENT) elif all(map(lambda x: isinstance(x, blue.ThingType), elements)): return View(elements, f'{self.__NAME}.{attr}', self.__PARENT) elif all(map(lambda x: isinstance(x, (FunctionType, MethodType)), elements)): return FunctionHandle(elements, f'{self.__NAME}.{attr}', self.__PARENT) elif all(map(lambda x: x is None, elements)): return [None] * len(elements) # [] else: return elements else: try: #elements = list((item for view in map(lambda x: x.__getattr__(attr), self) for item in view)) elements = list((item for view in map(lambda x: x.__getattribute__(attr), self) for item in view)) except AttributeError as error: raise AttributeError(f'{self}.{attr} cannot be retrieved.') from None return View(elements, f'{self.__NAME}.{attr}', self.__PARENT) @blue.restrict def __setattr__(self, attr: str, val: object) -> None: """ This method passes the setting of attributes to each individual Thing contained in View. Depending on whether ``val`` is a list/View containing values for each Thing individually or a singular value, ``val`` is either unrolled along the first axis or given fully for each assignment. >>> # keeping the view object from __getattr__ >>> view.bodies[1:].name = ['intermediate_index', 'intermediate_middle', 'intermediate_ring', 'intermediate_pinky'] >>> view.bodies.name ['intermediate_index', 'intermediate_middle', 'intermediate_ring', 'intermediate_pinky'] .. code-block:: mxml :caption: XML structure <body name="palm"> <body name="thumb"> <body name="distal_(0)" /> </body> <body name="index"> <body name="intermediate_index"> <body name="distal_(1)" /> </body> </body> <body name="middle"> <body name="intermediate_middle"> <body name="distal_(2)" /> </body> </body> <body name="ring"> <body name="intermediate_ring"> <body name="distal_(3)" /> </body> </body> <body name="pinky"> <body name="intermediate_pinky"> <body name="distal_(4)" /> </body> </body> </body> .. note:: If values should be unrolled over all Things, ``vals`` must have the same length as the View! Parameters ---------- attr : str The name of the attribute to be set. val : object Either a list View or other iterable that contains different values for all children or one value of any type that is set for all. """ if attr in self.__RESERVED: return super().__setattr__(attr, val) else: view_attr = self.__getattr__(attr) for element, elem_val in zip(self, self.__nesting_value(view_attr, val)): element.__setattr__(attr, elem_val) def __delattr__(self, attr): """ This method detaches the attributes and deletes them afterwards """ view_attr = self.__getattr__(attr) if isinstance(view_attr, blue.ViewType): view_attr.parent.detach.distribute(view_attr) self.__PARENT._decouple_descendants() def __delitem__(self, key): """ This method detaches the attributes and deletes them afterwards """ view_key = self[key] if isinstance(view_key, blue.ViewType): view_key.parent.detach.distribute(view_key) view_key.parent._decouple_descendants() self.__PARENT._decouple_descendants()
[docs] def apply(self, function, *args, **kwargs) -> None|list: """ Applies a function to all Things contained in View. Arguments and keyword arguments are passed to each individual call in an all to all relation. >>> # keeping the view object from __setattr__ >>> renaming = lambda x: x.__setattr__('name', f'distal_{x.parent.parent.name}') >>> view.bodies.bodies.apply(renaming) >>> view.bodies.bodies.name ['distal_index', 'distal_middle', 'distal_ring', 'distal_pinky'] Parameters ---------- function : Callable Functions methods and classes are valid arguments. *args Arguments passed to each individual call. **kwargs Kexword arguments passed to each individual call. Returns ------- None | list If any individual call results in a value that is not None, a list of all returned values gets returned, None otherwise. """ results = [function(element) for element in self] if any(x is not None for x in results): return results
def __nesting_value(self, view_attr, val): """ Thie method is used to create an iterator for value assignment that either creates a many to one or a one to one relation, depending on the new values shape and the current attributes shape. """ view_structure = self.__nesting_structure(view_attr) val_structure = self.__nesting_structure(val) if isinstance(val, np.ndarray) and self.__compatible_numpy(view_structure, val): return np.rollaxis(val, axis=0) elif self.__compatible_nesting(view_structure, val_structure): return val elif isinstance(val, blue.ViewType): if len(val) != 1: raise ValueError(f'To set a View attribute to another View both sizes must be identical or the other Views size must be one, got sizes {len(view_structure)} and {len(val_structure)}.') else: return (val[0] for _ in range(len(self))) else: return (val for _ in range(len(view_structure))) @classmethod def __nesting_structure(cls, element): """ This method retrieves a representation of the elements nesting structure. """ if isinstance(element, np.ndarray): return element.shape elif isinstance(element, str): return str elif isinstance(element, View): return cls.__nesting_structure(list(element)) elif hasattr(element, '__iter__'): return element.__class__(map(cls.__nesting_structure, element)) else: return element.__class__ @staticmethod def __compatible_nesting(view_structure, val_structure): """ This method determines whether the two nesting structures are compatible for a one to one assignment relation for lists and other non numpy iterables. """ val_class = val_structure if isinstance(val_structure, type) else val_structure.__class__ if not hasattr(val_structure, '__len__') or issubclass(val_class, str): return False elif len(val_structure) != len(view_structure): return False elif (not hasattr(val_structure, '__contains__') or view_structure not in val_structure) and val_structure != view_structure: return False else: return True @staticmethod def __compatible_numpy(view_structure, val): """ This method determines whether the two nesting structures are compatible for a one to one assignment relation for numpy arrays. """ if val.shape[0] == len(view_structure) and val.ndim > 1: if all(isinstance(view, tuple) and view == val.shape[1:] for view in view_structure): return True elif all(isinstance(view, (list, tuple)) and len(view) == val.shape[1] for view in view_structure): return True return False
[docs] class FunctionHandle(blue.FunctionHandleType): """ This class is used to handle access to a function or method called from a :class:`View`. >>> palm.bodies View[5|palm.bodies] >>> palm.bodies.shift ViewFunction[5|palm.bodies.shift] Arguments given to the function or method call are passed to each individual Thing in a many to one relation. >>> palm.bodies.pos [array([0., 0., 0.], dtype=float32), array([0., 0., 0.], dtype=float32), array([0., 0., 0.], dtype=float32), array([0., 0., 0.], dtype=float32), array([0., 0., 0.], dtype=float32)] >>> many_to_one = palm.bodies.shift([2, 0, -1]) >>> many_to_one View[5|palm.bodies.shift(...)] >>> many_to_one.pos [array([ 2., 0., -1.], dtype=float32), array([ 2., 0., -1.], dtype=float32), array([ 2., 0., -1.], dtype=float32), array([ 2., 0., -1.], dtype=float32), array([ 2., 0., -1.], dtype=float32)] If the :meth:`distributed` over all Things contained in the parenting View in a one to one relation by using the distribute submethod. >>> one_to_one = palm.bodies.shift.distribute([[0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1]]) >>> one_to_one.pos [array([0., 0., 1.], dtype=float32), array([0., 1., 0.], dtype=float32), array([0., 1., 1.], dtype=float32), array([1., 0., 0.], dtype=float32), array([1., 0., 1.], dtype=float32)] If the result of all method or function calls are None, None is returned. >>> palm.bodies.attach() None If all results are Things, a View of them is returned. This allows for functions and methods that return Things to be chained. >>> palm.bodies.copy() View[5|palm.bodies.copy(...)] >>> palm.bodies.shift([0, 0, 3]).rotate(alpha=PI).copy() View[5|palm.bodies.shift(...).rotate(...).copy(...)] Otherwise a list of all individually returned values is returned. >>> palm.bodies._mujoco_specs() [{'name': 'thumb'}, {'name': 'index'}, {'name': 'middle'}, {'name': 'ring'}, {'name': 'pinky'}] """
[docs] def __init__(self, functions: list, name: str, parent: blue.ThingType) -> None: """ The FunctionHandle takes the functions or methods to be called, the name by which it was called and the initial parent from which the first :class:`View` was called. Parameters ---------- functions : list All functions or methods from the Views Things. name : str The name of the called function or method. parent : blue.ThingType The parent from which the first View was called. """ self.__NAME = name self.__PARENT = parent self.__FUNCTIONS = functions
def __repr__(self) -> str: """ Represents the View as a string. Returns ------- str The representing string contains a representation of the call that was used to get the View. """ return f'ViewFunction[{len(self)}|{self.__PARENT.name}.{self.__NAME}]' def __iter__(self): """ Yields ------ Callable The functions or methods Things that were requested from the View. """ for functions in self.__FUNCTIONS: yield functions def __len__(self) -> int: """ Returns ------- int The number of """ return len(self.__FUNCTIONS) def __call__(self, *args, **kwargs) -> None|list: """ The call is redirected to each function or method in the FunctionHandle. All arguments are passed on all calls in a many to one relation. Parameters ---------- *args The arguments to be passed to the call. **kwargs The keywordarguments to be passed to the call. Returns ------- None | list | View The aggregated result of each call. """ result = [functions(*args, **kwargs) for functions in self] return self._convert_result(result)
[docs] def distribute(self, *args, **kwargs) -> None|list: """ The call is redirected to each function or method in the FunctionHandle. The arguments are unrolled along the first axis and distributed over all calls in a one to one relation. Parameters ---------- *args The arguments to be passed to the call. **kwargs The keywordarguments to be passed to the call. Returns ------- None | list | View The aggregated result of each call. """ kw_list = [dict(zip(kwargs.keys(), vals)) for vals in zip(*kwargs.values())] if args and kwargs: assert all(len(self) == l for l in map(len, args)) assert len(self) == len(kw_list) result = [function(*arg, **kwarg) for function, arg, kwarg in zip(self, zip(*args), kw_list)] elif args: assert all(len(self) == l for l in map(len, args)) result = [function(*arg) for function, arg in zip(self, zip(*args))] elif kwargs: assert len(self) == len(kw_list) result = [function(**kwarg) for function, kwarg in zip(self, kw_list)] else: result = [function() for function in self] return self._convert_result(result)
def _convert_result(self, result): if any(x is not None for x in result): if all(isinstance(x, blue.ThingType) for x in result): return View(result, f'{self.__NAME}(...)', self.__PARENT) else: return result else: return None
[docs] class AllView(blue.AllViewType, View): """ The AllView is analogous to the :class:`View` for all descendants instead of children. Attribute getting and setting is handeled for all valid descendants. A ThingsType attribute is then converted back to a normal view. .. code-block:: mxml :caption: XML Structure: <body name="A"> <body name="B" /> <geom mass="42.0" type="sphere" name="C" /> </body> .. code-block:: python :caption: Children and Descendants View Comparison: >>> world.bodies View[1|anonymous_model.bodies] >>> world.bodies.name ['A'] >>> world.all View[3|anonymous_model.all] >>> world.all.name ['A', 'B', 'C'] >>> world.all.bodies View[2|anonymous_model.all.bodies] >>> world.bodies.name ['A', 'B'] If only some descendants have a certain attribute, the view restricts to this valid subset. For example, only :class:`Geoms <blueprints.geoms.BaseGeom>` have mass, so retrieving ``all.mass`` excludes all non-Geoms. .. code-block:: python :caption: Children and Descendants View Comparison: >>> world.all.mass [42.0] """ __RESERVED = {'__ELEMENTS', '__NAME', '__PARENT', '__RESERVED', '__DESCENDANTS', '_View__ELEMENTS', '_View__NAME', '_View__PARENT', '_View__RESERVED', '_AllView__ELEMENTS', '_AllView__NAME', '_AllView__PARENT', '_AllView__RESERVED', '_AllView__DESCENDANTS'}
[docs] @blue.restrict def __init__(self, descendants: dict, parent: blue.ThingType) -> None: """ Parameters ---------- descendants : dict The descendants property invoked on ``parent`` parent : blue.ThingType The parent Thing from which ``.all`` was retrieved. """ elements = [] for desc_dict in descendants.values(): elements.extend(desc_dict['descendants']) super().__init__(elements=elements, name='all', parent=parent) self.__DESCENDANTS = descendants
def __getattr__(self, attr: str): if attr in self.__RESERVED: return super().__getattribute__(attr) if attr in self.__DESCENDANTS: return View(self.__DESCENDANTS[attr]['descendants'], f'{self._View__NAME}.{attr}', self._View__PARENT) else: if not all(hasattr(thing, attr) for thing in self): elements = list(filter(lambda x: hasattr(x, attr), self)) view = View(elements, name=self._View__NAME, parent=self._View__PARENT) return view.__getattr__(attr) else: return super().__getattr__(attr) @blue.restrict def __setattr__(self, attr: str, val: object) -> None: super().__setattr__(attr, val) if attr in self.__RESERVED or all(hasattr(thing, attr) for thing in self): super().__setattr__(attr, val) else: elements = list(filter(lambda x: hasattr(x, attr), self)) view = View(elements, name=self._View__NAME, parent=self._View__PARENT) view.__setattr__(attr, val)
[docs] class LatticeView(blue.LatticeViewType): """ A LatticeView is created whenever a :class:`Lattice <blueprints.utils.lattice.Lattice>` is indexed. Attribute getter and setter are handled through this View similar to :class:`View`. For details have a look at :class:`Lattice <blueprints.utils.lattice.Lattice>`. """ __RESERVED = {'_LatticeView__THINGS', '_LatticeView__KEYS', '_LatticeView__KEY_STR', '_LatticeView__PARENT'}
[docs] def __init__(self, things: list, parent: blue.LatticeType, keys: tuple[int|slice] = ()): """ Parameters ---------- things : list The indexed Things of the parent Lattice parent : blue.LatticeType The Lattice which was indexed to create the LatticeView keys : tuple[int | slice] The keys used to index the Lattice """ def parse(key): if isinstance(key, int): return str(key) elif isinstance(key, slice): indecies = [] if key.step is not None: indecies.insert(0, str(key.step)) if key.stop is not None: indecies.insert(0, str(key.stop)) else: indecies.insert(0, '') if key.start is not None: indecies.insert(0, str(key.start)) else: indecies.insert(0, '') return ':'.join(indecies) else: raise NotImplemented self.__KEY_STR = ', '.join(map(parse, keys)) self.__KEYS = keys self.__THINGS = things if isinstance(things, list) else [things] self.__PARENT = parent
@property def n_dim(self) -> int: """ The number of dimensions in the Lattice Returns ------- int """ things = self.__THINGS n = 0 while isinstance(things, list): n += 1 things = things[0] return n @blue.restrict def __getitem__(self, keys: int|slice|tuple[int|slice]) -> blue.LatticeViewType|blue.MoveableThingType: if isinstance(keys, tuple) and len(keys) > self.n_dim: raise IndexError(f'Lattice {self} has {self.n_dim} axes but {len(keys)} were indexed.') if isinstance(keys, (int, slice)): keys = (keys,) def rec(things, keys): if len(keys) == 0: return things else: key, keys = keys[0], keys[1:] if isinstance(key, int): return rec(things[key], keys) else: return [rec(thing, keys) for thing in things[key]] if all(isinstance(key, int) for key in keys) and len(keys) == self.n_dim: return rec(self.__THINGS, keys) return blue.LatticeView(rec(self.__THINGS, keys), self.__PARENT, keys) def __iter__(self) -> blue.ThingType: queue = deque(self.__THINGS) while queue: item = queue.popleft() if isinstance(item, list): queue.extendleft(reversed(item)) else: yield item def __len__(self) -> int: """ Returns ------- int The number of Things contained in the LatticeView. """ things = self.__THINGS l = 1 if things else 0 while isinstance(things, list): l *= len(things) things = things[0] return l def __repr__(self) -> str: """ Represents the View as a string. Returns ------- str The representing string contains a representation of the call that was used to get the View. """ if self.__KEY_STR: return f'LatticeView[{self.__KEY_STR}]' else: return 'LatticeView' def __getattr__(self, attr): if len(self) == 0: raise AttributeError(f'{self} is empty, attribute "{attr}" cannot be retrieved.') if all(hasattr(x, attr) for x in self): things = self._rec(lambda x: getattr(x, attr), list, self.__THINGS) if self._rec(lambda x: isinstance(x, blue.ViewType), all, things): things = list(self._rec(list, lambda x: list(chain(*x)), things)) return View(things, attr, self.__PARENT) elif self._rec(lambda x: isinstance(x, blue.ThingType), all, things): things = list(self._rec(lambda x: [x], lambda x: list(chain(*x)), things)) return View(things, attr, self.__PARENT) elif self._rec(lambda x: isinstance(x, (FunctionType, MethodType)), all, things): things = list(self._rec(lambda x: [x], lambda x: list(chain(*x)), things)) return FunctionHandle(things, attr, self.__PARENT) elif self._rec(lambda x: x is None, all, things): return [None] * len(self) # [] else: return things else: try: things = self._rec(lambda x: x.__getattribute__(attr), list, self.__THINGS) except AttributeError as error: raise AttributeError(f'{self}.{attr} cannot be retrieved.') from None return View(things, attr, self.__PARENT) def __setattr__(self, attr, value): if attr in self.__RESERVED: super().__setattr__(attr, value) else: view_attr = self.__getattr__(attr) view_structure = self.__nesting_structure(view_attr) val_structure = self.__nesting_structure(value) if view_structure == val_structure: def rec(func, things, values): if isinstance(things, list): for thing, value in zip(things, values): rec(func, thing, value) else: func(things, values) rec(lambda thing, value: thing.__setattr__(attr, value), self.__THINGS, value) else: def rec(func, things): if isinstance(things, list): for thing in things: rec(func, thing) else: func(things) rec(lambda thing: thing.__setattr__(attr, value), self.__THINGS) @classmethod def _rec(cls, func, aggregate, things): """ This classmethod is a helper to apply a function on individual Things and aggregate the results. """ if isinstance(things, list): return aggregate(cls._rec(func, aggregate, thing) for thing in things) else: return func(things) @classmethod def __nesting_structure(cls, element): """ This method retrieves a representation of the elements nesting structure. """ if isinstance(element, np.ndarray): return element.shape elif isinstance(element, str): return str elif isinstance(element, View): return cls.__nesting_structure(list(element)) elif hasattr(element, '__iter__'): return element.__class__(map(cls.__nesting_structure, element)) else: return element.__class__