在 Python 中使用 setter 和 getter 保护 numpy 属性

Protecting numpy attributes using setters and getters in Python

我 运行 遇到了保护我的属性(一个 numpy 数组)的问题。我想我明白为什么会出现这个问题,但我不确定如何防止它发生。特别是因为我的代码将被相对缺乏经验的程序员使用。

简而言之:,我有一个 class 具有我想确保其某些属性的属性。为此,我使用了一个私有内部变量以及 getters 和 setters。然而,并非一切都如我所愿,当设置了一个属性切片时,保护不起作用。

详细信息: 这是 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)

现在,当我设置和获取 our_attribute 时,它应该受到保护,请参见以下示例:

# 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')

但是,当我处理属性的切片(视图)时,奇怪的事情发生了。

# 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)

发生了以下事情:

  1. 它调用 getter 函数两次,(一次用于 our_object.our_attribute[0],一次用于 print
  2. 该属性似乎不受保护,因为出现负数。
  3. 甚至私有属性似乎也不受保护,因为
print('  even the private _our_attribute:', our_object._our_attribute, '\n')

也包括负数!

我的推测:

  1. 属性的一部分不受保护,因为我们访问它不会进入 setter 函数。 Numpy 允许我们直接访问切片并更改内容。 (我们从getter函数中获取our_attribute,现在我们直接作用于这个数组对象,它允许设置一个切片,由Numpy处理,而不是我们的class作为对象我们正在执行的操作现在只是一个数组。
  2. getter 函数包括 return self._our_attribute,这不是复制操作,现在 self.our_attributeself._our_attribute 都指向同一个位置。如果您更改两者中的任何一个,其他更改也会发生,因此即使我们无意更改它,我们最终也会更改我们的私有属性。

现在我的问题:

  1. 我的猜测是正确的,还是我弄错了。
  2. 我假设我可以不同地定义 setter 以确保私有属性与 public 分开:
@property
def our_attribute(self):
    return np.copy(self._our_attribute)

但是现在设置切片不会改变任何东西。

如何正确保护我的属性,使我可以更改属性的一部分并仍然保留保护。重要的是这种保护从外面是看不见的,以免混淆我的学生。

你的猜测基本上是正确的。 Python 中没有“保护”可变对象不被修改这样的事情,NumPy 数组是可变的。您可以使用普通 Python 列表做完全相同的事情:

@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]

我不太清楚你在这里想要什么,但如果你希望你的属性返回的 numpy 数组是不可变的,你可以在数组上设置 array.setflags(write=False)。只要在设置 write=False 标志后创建切片,这也会使数组切片不可变。

但是,如果您想要某种神奇的有界 NumPy 数组,其中对数组的任何操作都将强制执行您设置的边界,这可以通过 ndarray 子类实现,但并非易事。

不过,none 这些选项会阻止某人将基础数据重新转换为可变数组。

完全保护底层数据的唯一方法是使用只读 mmap 作为底层数组缓冲区。

但是 TL;DR 通过 property 访问 Python 对象并使该对象不可变并没有什么神奇之处。如果你真的想要一个不可变的数据类型,你必须使用一个不可变的数据类型。

感谢@Iguananaut 的明确回答。在这里,我只想在我之前展示的示例中实现她的答案。是的,这仍然没有真正受到保护,但这应该可以防止出现问题,除非人们开始摆弄内部变量。

我决定采用 array.setflags(write=False) 的方法,但尝试以更简单的方式进行。对下面的实现进行了以下 3 处更改:

  1. 复制getter函数中的私有属性,保证改变public属性的flag不会改变私有属性的flag。
  2. public 属性设置为不可写,以确保人们在尝试设置属性的一部分时会收到警告。 (保护工作没有这个,但如果有人设置你的 public 属性它不会抛出错误,它不会做任何事情)。
  3. 确保内部属性不可写。
# 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):
        # Set values to the attribute.
        self.our_attribute = np.random.random(n)

    @property
    def our_attribute(self):
        out = self._our_attribute.copy()  # --> Change 1
        out.setflags(write=False)  # --> Change 2
        return out

    @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)
        self._our_attribute.setflags(write=False)  # --> Change 3

# 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')

改变数组的一部分是不可能的,它将通过 VallueError: assignment destination is read-only.

# 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[-1] = -5
print('  our_attribute:', our_object.our_attribute)

现在有人可以尝试成为 'smart' 并更改他们获得的数组的写入标志,但如果他们这样做,他们不会影响私有属性 self._our_attribute 因为他们获得了从它进行深层复制,每次他们调用 public 属性时,他们都会再次收到副本,因此以下内容仍然会出错:

# 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.setflags(write=True)
our_object.our_attribute[-1] = -5
print('  our_attribute:', our_object.our_attribute)

在不直接触及私有属性的情况下避免这种情况的唯一方法是:

# Now modify a slice of the copy of the attribute, at set the full attribute to the changed copy.
print('Modify a slice of the attribute.')
attribute = our_object.our_attribute.copy()
attribute[-1] = -5
our_object.our_attribute = attribute
print('  our_attribute:', our_object.our_attribute)

但这再次通过我们的 setter 函数,就像我们喜欢的那样。