How to make the elements of a NumPy array property settable?

Question:

I have a property of a Python object that returns an array.
Now, I can set the setter of that property such that the whole array is settable.
However, I’m missing how to make the elements by themselves settable through the property.

I would expect from a user perspective (given an empty SomeClass class):

>>> x = SomeClass()
>>> x.array = [1, 2, 3]
>>> x.array[1] = 4
>>> print (x.array)
[1, 4, 3]

Now, suppose that SomeClass.array is a property defined as

class SomeClass(object):
    def __init__(self, a):
        self._a = a

    @property
    def array(self):
        return self._a
    @array.setter
    def array(self, a):
        self._a = a

Everything still works as above. Also if I force simple NumPy arrays on the setter.

However, if I replace the return self._a with a NumPy function (that goes in a vectorised way through the elements) and I replace self._a = a with the inverse function, of course the entry does not get set anymore.

Example:

import numpy as np

class SomeClass(object):
    def __init__(self, a):
        self._a = np.array(a)

    @property
    def array(self):
        return np.sqrt(self._a)
    @array.setter
    def array(self, a):
        self._a = np.power(a, 2)

Now, the user sees the following output:

>>> x = SomeClass([1, 4, 9])
>>> print (x.array)
array([1., 2., 3.])
>>> x.array[1] = 13
>>> print (x.array)    # would expect an array([1., 13., 3.]) now!
array([1., 2., 3.])

I think I understand where the problem comes from (the array that NumPy creates during the operation gets its element changed but it doesn’t have an effect on the stored array).

What would be a proper implementation of SomeClass to make single elements of the array write-accessible individually and thus settable as well?

Thanks a lot for your hints and help,
TheXMA

The points @Jaime made below his answer helped me a lot! Thanks!

Asked By: AdrianO

||

Answers:

Since arrays are mutable objects, the individual items are settable even without a setter function:

>>> class A(object):
...     def __init__(self, a):
...         self._a = np.asarray(a)
...     @property
...     def arr(self):
...         return self._a
...
>>> a = A([1,2,3])

>>> a.arr
array([1, 2, 3])

>>> a.arr = [4,5,6] # There is no setter...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

>>> a.arr[1] = 7 # ...but the array is mutable
>>> a.arr
array([1, 7, 3])

This is one of the uses of tuples vs. lists, since the latter are mutable, but the former aren’t. Anyway, to answer your question: making individual items settable is easy, as long as your getter returns the object itself.

The fancier performance in your second example doesn’t seem easy to get in any simple way. I think you could make it happen by making your SomeClass.array attribute be a custom class, that either subclasses ndarray or wraps an instance of it. Either way would be a lot of nontrivial work.

Answered By: Jaime

To make array elements settable when the underlying object isn’t returned you must use a subclass of both the property and ndarray classes. The __get__ method of the property class returns before the __getitem__ or __setitem__ methods of the ndarray are called, so it can be used to patch in the desired getters and setters for the array. With the appropriate subclasses, all that needs to change for the implementation in your own class is the addition of a keyword and logic for handling the indexing. Your example would look like the following using the DescriptorArray and ArrayProperty classes defined below:

import numpy as np

class SomeClass():
    def __init__(self, a):
        self._a = np.array(a)

    @ArrayProperty
    def array(self, key=...):
        return np.sqrt(self._a[key])
    @array.setter
    def array(self, a, key=...):
        self._a[key] = np.power(a, 2)

The __getitem__ and __setitem__ methods of the array use the getter and setter defined in your class, and the results are as expected:

>>> x = SomeClass([1., 4., 9.])
>>> print(x.array)
[1. 2. 3.]
>>> x.array[1] = 13
>>> print(x.array)
[ 1. 13.  3.]
>>> print(x._a)
[  1. 169.   9.]

The ndarray and property subclasses used in this example were defined as follows:

class DescriptorArray(np.ndarray):
    item_getter = item_setter = None
    def __getitem__(self, key):
        if self.item_getter is None:
            return super().__getitem__(key)
        else:
            return self.item_getter(key)
    def __setitem__(self, key, value):
        if self.item_setter is None:
            super().__setitem__(key, value)
        else:
            self.item_setter(key, value) # set underlying object
            self.base[key] = self[key] # update copy of processed result

class ArrayProperty(property):
    def __get__(self, obj, objtype):
        # Get Array
        array = super().__get__(obj, objtype)
        # Define Item Getter and Setter
        def item_getter(key):
            return self.fget(obj, key)
        def item_setter(key, value):
            if self.fset is None:
                self.__set__(obj, value) # to raise AttributeError
            self.fset(obj, value, key)
        # Return DescriptorArray
        dsc_array = array.view(DescriptorArray) # view casting
        dsc_array.item_getter = item_getter
        dsc_array.item_setter = item_setter
        return dsc_array

The code for DescriptorArray has some quirks specific to NumPy ndarrays since their subclasses typically propagate through other operations (so it needs to still work when the getter and setter are undefined), but the general procedure of overriding the __getitem__ and __setitem__ methods of the object returned by the __get__ method should work for other container types.

Answered By: coro
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.