Module mpython.struct
Classes
class Struct (*args, **kwargs)
-
Expand source code
class Struct(_DictMixin, WrappedArray): """ Struct array, compatible with matlab structs. ```python # Instantiate from size Struct(N, M, ...) Struct([N, M, ...]) Struct.from_shape([N, M, ...]) # Instantiate from existing struct array # (or list/cell of dictionaries) Struct(struct_like) Struct.from_any(struct_like) # Instantiate from dictionary Struct(a=x, b=y) Struct({"a": x, "b": y}) Struct.from_any({"a": x, "b": y}) ``` The following field names correspond to existing attributes or methods of `Struct` objects and are therefore protected. They can still be used as field names, but only through the dictionary syntax (`s["shape"]`), not the dot syntax (`s.shape`): * `ndim -> int` : number of dimensions * `shape -> list[int]` : array shape * `size -> int` : number of elements * `reshape() -> Struct` : struct array with a different shape * `keys() -> list[str]` : field names * `values() -> list` : values (per key) * `items() -> [str, list]` : (key, value) pairs * `get() -> list` : value (per element) * `setdefault()` : sets default value for field name * `update()` : update fields from dictionary-like * `as_num -> raise` : interpret object as a numeric array * `as_cell -> raise` : interpret object as a cell array * `as_struct -> Struct` : interpret object as a struct array * `as_dict() -> dict` : convert to plain dictionary * `from_shape() -> Struct` : build a new empty struct * `from_any() -> Struct` : build a new struct by (shallow) copy * `from_cell() -> Struct` : build a new struct by (shallow) copy The following field names are protected because they have a special meaning in the python language. They can still be used as field names through the dictionary syntax: | | | | | | | |-|-|-|-|-|-| | `as` | `assert` | `break` | `class` | `continue`| `def` | | `del` | `elif` | `else` | `except` | `False` | `finally` | | `for` | `from` | `global` | `if` | `import` | `in` | | `is` | `lambda` | `None` | `nonlocal`| `not` | `or` | | `pass` | `raise` | `return` | `True` | `try` | `while` | | `with` | `yield` | """ # NOTE # _DictMixin must have precedence over _WrappedArray so that its # method overload those from np.ndarray. This is why the # inheritence order is (_DictMixin, _WrappedArray). # List of public attributes and methods from the ndarray class that # we keep in Struct. I've tried to find the minimal set of attributes # required to not break the numpy api. _NDARRAY_ATTRS = ("ndim", "shape", "size", "reshape") _DelayedType = DelayedStruct @classmethod def _DEFAULT(self, shape: list = ()) -> np.ndarray: # if len(shape) == 0: # out = np.array(None) # out[()] = dict() # return out data = np.empty(shape, dtype=dict) opt = dict( flags=["refs_ok", "zerosize_ok"], op_flags=["writeonly", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: elem[()] = dict() return data def _fill_default(self): arr = np.ndarray.view(self, np.ndarray) flags = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite"]) with np.nditer(arr, **flags) as iter: for elem in iter: elem[()] = dict() return self def __new__(cls, *args, **kwargs) -> "Struct": kwargs["__has_dtype"] = False kwargs["__has_order"] = False mode, arg, kwargs = cls._parse_args(*args, **kwargs) if mode == "shape": obj = super().__new__(cls, shape=arg, dtype=dict)._fill_default() else: obj = cls.from_any(arg) obj.update(kwargs) return obj def _as_runtime(self) -> dict: if self.ndim == 0: data = np.ndarray.view(self, np.ndarray).item() data = MatlabType._to_runtime(data) return data if np.ndarray.view(self, np.ndarray).size == 0: return dict(type__="emptystruct") size = np.array([[*np.shape(self)]]) data = np.ndarray.view(self, np.ndarray) data = np.reshape(data, [-1], order="F") data = MatlabType._to_runtime(data) return dict(type__="structarray", size__=size, data__=data) @classmethod def _from_runtime(cls, objdict: dict, runtime=None) -> "Struct": if objdict["type__"] != "structarray": raise TypeError("objdict is not a structarray") size = np.array(objdict["size__"], dtype=np.uint64).ravel() if len(size) == 2 and size[0] == 1: # NOTE: should not be needed for Cell, as this should # have been taken care of by MPython, but I am keeping it # here for symmetry with Array and Struct. size = size[1:] data = np.array(objdict["data__"], dtype=object) data = data.reshape(size) try: obj = data.view(cls) except Exception: raise RuntimeError( f"Failed to construct Struct data:\n" f" data={data}\n objdict={objdict}" ) # recurse opt = dict( flags=["refs_ok", "zerosize_ok"], op_flags=["readonly", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: item = elem.item() for key, val in item.items(): item[key] = MatlabType._from_runtime(val, runtime) return obj @classmethod def from_shape(cls, shape=tuple(), **kwargs) -> "Struct": """ Build a struct array of a given size. Parameters ---------- shape : list[int] Input shape. Other Parameters ---------------- order : {"C", "F"} | None, default="C" Memory layout. * `"C"` : row-major (C-style); * `"F"` : column-major (Fortran-style). Returns ------- struct : Struct New struct array. """ return cls(list(shape), **kwargs) @classmethod def from_any(cls, other, **kwargs) -> "Struct": """ * Convert a dict-like object to struct; or * Convert an array of dict-like objects to a struct array. Parameters ---------- other : DictLike | ArrayLike[DictLike] object to convert. Other Parameters ---------------- order : {"C", "F", "A", "K"} | None, default=None Memory layout. * `"C"` : row-major (C-style); * `"F"` : column-major (Fortran-style); * `"A"` : (any) `"F"` if a is Fortran contiguous, `"C"` otherwise; * `"K"` : (keep) preserve input order; * `None`: preserve input order if possible, `"C"` otherwise. copy : bool | None, default=None Whether to copy the underlying data. * `True` : the object is copied; * `None` : the the object is copied only if needed; * `False`: raises a `ValueError` if a copy cannot be avoided. owndata : bool, default=None If `True`, ensures that the returned `Struct` owns its data. This may trigger an additional copy. Returns ------- struct : Struct Converted structure. """ if isinstance(other, dict) and "type__" in other: # matlab object return cls._from_runtime(other) kwargs["dtype"] = dict # prepare for copy owndata = kwargs.pop("owndata", False) copy = None if owndata else kwargs.pop("copy", None) inp = other # convert to array[dict] other = np.asanyarray(other, **kwargs) other = cls._unroll_build(other) # check all items are dictionaries arr = np.ndarray.view(other, np.ndarray) opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readonly"]) with np.nditer(arr, **opt) as iter: if not all(isinstance(elem.item(), dict) for elem in iter): raise TypeError("Not an array of dictionaries") # view as Struct other = np.ndarray.view(other, cls) # copy (after view so that output owns data if copy=True) other = _copy_if_needed(other, inp, copy) # take ownership if owndata: tmp = other other = cls(tmp.shape, dtype=tmp.dtype, strides=tmp.strides) other[...] = tmp # nested from_any opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readonly"]) with np.nditer(other, **opt) as iter: for elem in iter: item: dict = elem.item() for k, v in item.items(): item[k] = MatlabType.from_any(v) return other @classmethod def from_cell(cls, other, **kwargs) -> "Struct": """See `from_any`.""" Cell = _imports.Cell if not isinstance(other, Cell): raise TypeError(f"Expected a {Cell} but got a {type(other)}.") return cls.from_any(other, **kwargs) @classmethod def _unroll_build(cls, other): # The logic here is that we may sometimes end up with arrays of # arrays of dict rather than a single deep array[dict] # (for example when converting cells of cells of dict). # To circumvent this, we find elements that are arrays, convert # them to lists, and recurse. rebuild = False arr = np.ndarray.view(other, np.ndarray) flags = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite"]) with np.nditer(arr, **flags) as iter: for elem in iter: item = elem.item() if isinstance(item, np.ndarray): item = np.ndarray.view(item, dict, np.ndarray) if item.ndim == 0: item = item.item() else: item = item.tolist() elem[()] = item rebuild = True if rebuild: # Recurse (we may have arrays of arrays of arrays...) return cls._unroll_build(other) return other @property def as_struct(self) -> "Struct": return self def __repr__(self): if self.ndim == 0: # Scalar struct -> display as a dict return repr(np.ndarray.view(self, np.ndarray).item()) else: return super().__repr__() def as_dict(self, keys=None) -> dict: """ Convert the object into a plain dict. * If a struct, return the underlying dict (no copy, is a view) * If a struct array, return a dict of Cell (copy, not a view) """ self._ensure_defaults_are_set(keys) # NOTE # The `keys` argument is only used in `__getattr__` to avoid # building the entire dictionary, when the content of a single # key is eventually used. # scalar struct -> return the underlying dictionary arr = np.ndarray.view(self, np.ndarray) if arr.ndim == 0: asdict = arr.item() if keys is not None: asdict = {key: asdict[key] for key in keys} return asdict # otherwise -> reverse array/dict order -> dict of cells of values if keys is None: keys = self._allkeys() elif isinstance(keys, str): keys = [keys] opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite"]) asdict = {key: [] for key in keys} with np.nditer(arr, **opt) as iter: for elem in iter: item = elem.item() for key in keys: asdict[key].append(item[key]) Cell = _imports.Cell for key in keys: asdict[key] = Cell.from_any(asdict[key]) return asdict def _allkeys(self): # Return all keys present across all elements. # Keys are ordered by (1) element (2) within-element order mock = {} arr = np.ndarray.view(self, np.ndarray) opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readonly"]) with np.nditer(arr, **opt) as iter: for elem in iter: mock.update({key: None for key in elem.item().keys()}) return mock.keys() def _ensure_defaults_are_set(self, keys=None): """ If a new key is set in an array element, this function ensures that all other elements are assigned a default value in the new key. """ arr = np.ndarray.view(self, np.ndarray) if arr.ndim == 0: if keys: item: dict = arr.item() for key in keys: item.setdefault(key, _empty_array()) if keys is None: keys = self._allkeys() elif isinstance(keys, str): keys = [keys] opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readonly"]) with np.nditer(arr, **opt) as iter: for elem in iter: item: dict = elem.item() for key in keys: item.setdefault(key, _empty_array()) # -------------- # Bracket syntax # -------------- def __getitem__(self, index): if isinstance(index, str): try: return getattr(self, index) except AttributeError as e: raise KeyError(str(e)) else: obj = WrappedArray.__getitem__(self, index) if not isinstance(obj, (Struct, DelayedStruct)): # We've indexed a single element, but we do not want # to expose the underlying dictionary. Instead, # we return an empty-sized view of the element, which # is still of type `Struct`. if not isinstance(index, tuple): index = (index,) index += (None,) obj = np.ndarray.__getitem__(self, index) obj = np.reshape(obj, []) return obj def __setitem__(self, index, value): value = MatlabType.from_any(value) if isinstance(index, str): setattr(self, index, value) else: WrappedArray.__setitem__(self, index, value) self._ensure_defaults_are_set() def __delitem__(self, index): if isinstance(index, str): try: return delattr(self, index) except AttributeError as e: raise KeyError(str(e)) return WrappedArray.__delitem__(self, index) # ---------- # Dot syntax # ---------- def __getattribute__(self, key): # Hide public numpy attributes asnumpy = np.ndarray.view(self, np.ndarray) if ( hasattr(asnumpy, key) and key[:1] != "_" and key not in type(self)._NDARRAY_ATTRS ): raise AttributeError(f"hide numpy.ndarray.{key}") return super().__getattribute__(key) def __getattr__(self, key): if key[:1] == "_": raise AttributeError( f"{type(self).__name__} object has no attribute '{key}'" ) try: return _DictMixin.__getitem__(self, key) except KeyError as e: raise AttributeError(str(e)) def __setattr__(self, key, value): if key[:1] == "_": super().__setattr__(key, value) self._ensure_defaults_are_set() return try: if key in self._NDARRAY_ATTRS: # SyntaxWarning: overwriting numpy attributes warnings.warn( f"Field name '{key}' conflicts with an existing numpy attribute in {type(self).__name__}. " f"To avoid ambiguity, consider using dictionary-style access: {type(self).__name__}['{key}'] instead of dot notation.", SyntaxWarning, stacklevel=2, ) _DictMixin.__setitem__(self, key, value) self._ensure_defaults_are_set() return except KeyError as e: raise AttributeError(str(e)) def __delattr__(self, key): if key[:1] == "_": return super().__delattr__(key) try: return _DictMixin.__delitem__(self, key) except KeyError as e: raise AttributeError(str(e))
Struct array, compatible with matlab structs.
# Instantiate from size Struct(N, M, ...) Struct([N, M, ...]) Struct.from_shape([N, M, ...]) # Instantiate from existing struct array # (or list/cell of dictionaries) Struct(struct_like) Struct.from_any(struct_like) # Instantiate from dictionary Struct(a=x, b=y) Struct({"a": x, "b": y}) Struct.from_any({"a": x, "b": y})
The following field names correspond to existing attributes or methods of
Struct
objects and are therefore protected. They can still be used as field names, but only through the dictionary syntax (s["shape"]
), not the dot syntax (s.shape
):ndim -> int
: number of dimensionsshape -> list[int]
: array shapesize -> int
: number of elementsreshape() -> Struct
: struct array with a different shapekeys() -> list[str]
: field namesvalues() -> list
: values (per key)items() -> [str, list]
: (key, value) pairsget() -> list
: value (per element)setdefault()
: sets default value for field nameupdate()
: update fields from dictionary-likeas_num -> raise
: interpret object as a numeric arrayas_cell -> raise
: interpret object as a cell arrayas_struct -> Struct
: interpret object as a struct arrayas_dict() -> dict
: convert to plain dictionaryfrom_shape() -> Struct
: build a new empty structfrom_any() -> Struct
: build a new struct by (shallow) copyfrom_cell() -> Struct
: build a new struct by (shallow) copy
The following field names are protected because they have a special meaning in the python language. They can still be used as field names through the dictionary syntax:
as
assert
break
class
continue
def
del
elif
else
except
False
finally
for
from
global
if
import
in
is
lambda
None
nonlocal
not
or
pass
raise
return
True
try
while
with
yield
Ancestors
- mpython.core.mixin_types._DictMixin
- collections.abc.MutableMapping
- collections.abc.Mapping
- collections.abc.Collection
- collections.abc.Sized
- collections.abc.Iterable
- collections.abc.Container
- WrappedArray
- numpy.ndarray
- AnyWrappedArray
- AnyMatlabArray
- MatlabType
Static methods
def from_any(other, **kwargs) ‑> Struct
-
- Convert a dict-like object to struct; or
- Convert an array of dict-like objects to a struct array.
Parameters
other
:DictLike | ArrayLike[DictLike]
- object to convert.
Other Parameters
order
:{"C", "F", "A", "K"} | None
, default=None
-
Memory layout.
"C"
: row-major (C-style);"F"
: column-major (Fortran-style);"A"
: (any)"F"
if a is Fortran contiguous,"C"
otherwise;"K"
: (keep) preserve input order;None
: preserve input order if possible,"C"
otherwise.
copy
:bool | None
, default=None
-
Whether to copy the underlying data.
True
: the object is copied;None
: the the object is copied only if needed;False
: raises aValueError
if a copy cannot be avoided.
owndata
:bool
, default=None
- If
True
, ensures that the returnedStruct
owns its data. This may trigger an additional copy.
Returns
struct
:Struct
- Converted structure.
def from_cell(other, **kwargs) ‑> Struct
-
See
from_any
. def from_shape(shape=(), **kwargs) ‑> Struct
-
Build a struct array of a given size.
Parameters
shape
:list[int]
- Input shape.
Other Parameters
order
:{"C", "F"} | None
, default="C"
-
Memory layout.
"C"
: row-major (C-style);"F"
: column-major (Fortran-style).
Returns
struct
:Struct
- New struct array.
Instance variables
prop as_struct : Struct
-
Expand source code
@property def as_struct(self) -> "Struct": return self
Methods
def as_dict(self, keys=None) ‑> dict
-
Expand source code
def as_dict(self, keys=None) -> dict: """ Convert the object into a plain dict. * If a struct, return the underlying dict (no copy, is a view) * If a struct array, return a dict of Cell (copy, not a view) """ self._ensure_defaults_are_set(keys) # NOTE # The `keys` argument is only used in `__getattr__` to avoid # building the entire dictionary, when the content of a single # key is eventually used. # scalar struct -> return the underlying dictionary arr = np.ndarray.view(self, np.ndarray) if arr.ndim == 0: asdict = arr.item() if keys is not None: asdict = {key: asdict[key] for key in keys} return asdict # otherwise -> reverse array/dict order -> dict of cells of values if keys is None: keys = self._allkeys() elif isinstance(keys, str): keys = [keys] opt = dict(flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite"]) asdict = {key: [] for key in keys} with np.nditer(arr, **opt) as iter: for elem in iter: item = elem.item() for key in keys: asdict[key].append(item[key]) Cell = _imports.Cell for key in keys: asdict[key] = Cell.from_any(asdict[key]) return asdict
Convert the object into a plain dict.
- If a struct, return the underlying dict (no copy, is a view)
- If a struct array, return a dict of Cell (copy, not a view)