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

Posted

技术标签:

【中文标题】在 Python 中使用 setter 和 getter 保护 numpy 属性【英文标题】:Protecting numpy attributes using setters and getters in Python 【发布时间】:2021-12-02 18:53:49 【问题描述】:

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

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

详细说明:这是 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)

发生了以下事情:

    它调用 getter 函数两次,(一次用于our_object.our_attribute[0],一次用于print) 出现负数时,该属性似乎不受保护。 即使是私有属性似乎也没有受到保护
print('  even the private _our_attribute:', our_object._our_attribute, '\n')

也包括负数!

我的猜测:

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

现在我的问题:

    我的推测是正确的,还是我犯了错误。 我假设我可以以不同的方式定义 setter,以确保私有属性与公共属性分开:
@property
def our_attribute(self):
    return np.copy(self._our_attribute)

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

如何正确保护我的属性,以便我可以更改属性的一部分并仍然保留保护。重要的是,这种保护从外面看不到,以免让我的学生感到困惑。

【问题讨论】:

【参考方案1】:

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

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

    在getter函数中复制private属性,保证改变public属性的flag不会改变private属性的flag。 公共属性设置为不可写,以确保人们在尝试设置属性切片时会收到警告。 (没有这个保护工作,但如果有人设置你的公共属性,它不会抛出错误,它根本不会做任何事情)。 确保内部属性不可写。
# 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)

现在有人可以尝试“聪明”并更改他们获得的数组的写入标志,但如果他们这样做,他们不会影响私有属性self._our_attribute,因为他们从中获得了深层副本,并且每次他们调用公共属性时,他们都会再次收到副本,因此以下内容仍然会出现错误:

# 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 函数,就像我们喜欢的那样。

【讨论】:

【参考方案2】:

您的推测基本上是正确的。 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 子类实现,但并不重要。

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

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

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

【讨论】:

感谢您的快速回复,我很害怕这个。然后我将使用不可变类型。但事实上,我的希望寄托在“神奇的 NumPy 功能”上,至于真正的代码,只更改属性的某些部分确实是有意义的。您仍然可以对不可变类型执行此操作,但它会做更多的工作,假设您想要更改不可变类型的属性:1. 复制它和 2. 将副本重新转换为可变类型 3. 更改可变的部分对象 4. 将其重铸为不可变类型, 5. 使用不可变设置属性。

以上是关于在 Python 中使用 setter 和 getter 保护 numpy 属性的主要内容,如果未能解决你的问题,请参考以下文章

JS对象属性中get/set与getter/setter是什么

Python get 和 set 方法与 @property 装饰器

getter和setter方法

使用lombok注解@Getter @Setter方法代码编译成功,但是没有生成get,set方法

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

Laravel 和 vue axios get 方法返回 Getters & Setters?