Issue
I have run into an issue with protecting my attribute, a numpy array. I think that I understand why the issue appears, but I'm not sure on the way to prevent it from happening. Especially because my code will be used by relatively inexperienced programmers.
In Short:, I have a class with an attribute that I want to ensure some properties of. For that purpose I use a private internal variable and getters and setters. However, not everything goes the way I want it to, the protection does not work when a slice of the atribute is set.
In Detail: Here is the first part of the MWE:
# Importing modules.
import numpy as np
class OurClass(object):
"""
The class with an attribute that we want to protect.
Parameters
----------
n : int
The number of random numbers in the attribute.
Attributes
----------
our_attribute : array
The attribute that contains `n` random numbers between 0 and 1, will never be smaller than zero.
_our_attribute : array
The protected attributed that ensures that `our_attribute` is never smaller then zero.
(Normally I don't list this)
"""
def __init__(self, n):
self.our_attribute = np.random.random(n)
@property
def our_attribute(self):
return self._our_attribute
@our_attribute.setter
def our_attribute(self, value):
"""
When the attribute is set, every entry needs to be larger then zero.
Parameters
----------
value : array
The array that should replace our_attribute.
"""
print("Setter function is used")
self._our_attribute = np.clip(value, 0, np.inf)
Now when I'm setting and getting our_attribute
it should be protected, see the following examples:
# Lets create our object.
print('Create object:')
num = 5
our_object = OurClass(num)
print(' our_attribute:', our_object.our_attribute, '\n')
# Lets replace the setter function te verify that it works.
print('Change object:')
our_object.our_attribute = np.linspace(-5, 20, num)
print(' our_attribute:', our_object.our_attribute, '\n')
# Now modify the attribute using basic functions.
print('Modify using numpy functionality:')
our_object.our_attribute = our_object.our_attribute - 5
print(' our_attribute:', our_object.our_attribute, '\n')
However, the moment that I'm working on a slice (view) of the attribute, weird stuff happens.
# Now modify a slice of the attribute, we can do this because it is a numpy array.
print('Modify a slice of the attribute.')
our_object.our_attribute[0] = -5
print(' our_attribute:', our_object.our_attribute)
The following things happened:
- It calls the getter function twice, (once for
our_object.our_attribute[0]
and once in theprint
) - The attribute seems unprotected as a negative number appears.
- Even the private attribute seems unprotected as
print(' even the private _our_attribute:', our_object._our_attribute, '\n')
includes negative numbers as well!
My speculation:
- A slice of the attribute is not protected because we access it does not go into the setter function. Numpy allows us to directly access the slice and change the content. (We obtain
our_attribute
from the getter function, now we act directly upon this array object, which allows the setting of a slice, which is handled by Numpy, not our class as the object that we're acting upon is now simply an array. - The getter function includes
return self._our_attribute
which is NOT a copy action, now bothself.our_attribute
andself._our_attribute
point to the same location. If you change any of the two in place the other changes as well, hence we end up with a change in our private attribute even though we didn't intent to change it.
Now my Questions:
- Are my speculations correct, or did I make a mistake.
- I assume that I can define the setter differently to ensure that the private attribute becomes separate from the public one:
@property
def our_attribute(self):
return np.copy(self._our_attribute)
but now setting a slice will simply not change anything.
How do I properly protect my attribute in a way that I can change a slice of the attribute and still retain the protection. It is important that this protection is not visible from the outside, as to not confuse my students.
Solution
Your speculations are essentially correct. There is no such thing in Python as "protecting" a mutable object from modification, and NumPy arrays are mutable. You could do the exact same thing with plain Python lists:
@property
def my_list(self):
return self._my_list # = [1, 2, 3, 4]
@my_list.setter
def my_list(self, value):
self._my_list = [float('inf') if x < 0 else x for x in value]
It's not exactly clear to me what you want here, but if you want the numpy array returned by your attribute to be immutable you could set array.setflags(write=False)
on the array. This will also make slices of the array immutable so long as the slices are created after setting the write=False flag.
However, if you want some kind of magically bounded NumPy array, where any operation on the array will enforce the bounds you've set, this is possible with an ndarray subclass, but non-trivial.
Still, none of these options would prevent someone from just re-casting the underlying data to a mutable array.
The only way to completely protect the underlying data is to use a read-only mmap as the underlying array buffer.
But TL;DR there is nothing magical about accessing a Python object through property
that can make that object immutable. If you really want an immutable data type you have to use an immutable data type.
Answered By - Iguananaut
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.