如何使类属性不可变?

Posted

技术标签:

【中文标题】如何使类属性不可变?【英文标题】:How can I make class properties immutable? 【发布时间】:2017-09-15 03:23:23 【问题描述】:

@property 是定义 getter 的好方法。当属性是可变的时,返回的引用可用于以不受类定义控制的方式修改属性。我将使用香蕉架作为一个激励类比,但这个问题适用于任何包装容器的类。

class BananaStand:
    def __init__(self):
        self._money = 0
        self._bananas = ['b1', 'b2']

    @property
    def bananas(self):
        return self._bananas

    def buy_bananas(self, money):
        change = money
        basket = []
        while change >= 1 and self._bananas:
            change -= 1
            basket.append(self._bananas.pop())
            self._money += 1
        return change, basket

我希望香蕉摊位的游客为他们的香蕉付费。不幸的是,没有什么能阻止一只猴子(它不知道更好)拿走我的一根香蕉。猴子不用内部属性_banana,他们只是拿了一根香蕉不付钱。

def take_banana(banana_stand):
    return banana_stand.bananas.pop()

>>> stand = BananaStand()
>>> stand.bananas
['b1', 'b2']
>>> take_banana(stand)
'b2'
>>> stand.bananas
['b1']

这个类比有点傻,但是任何具有可变属性的类都无法防止意外破坏。在我的实际情况中,我有一个具有两个必须保持相同长度的数组属性的类。使用数组,没有什么能阻止用户将第二个数组拼接到第一个数组中并默默地破坏我的等大小不变量:

>>> from array import array
>>> x = array('f', [1,2,3])
>>> x
array('f', [1.0, 2.0, 3.0])
>>> x[1:2] = array('f', [4,5,6])
>>> x
array('f', [1.0, 4.0, 5.0, 6.0, 3.0])

当数组是一个属性时,也会发生同样的行为。

我可以想到两种避免问题的方法:

    子类数组并覆盖__setitem__。我对此很抗拒,因为我希望能够在内部使用这种数组拼接行为。 更改访问器以返回数组的深层副本。返回的数组仍然是可变的,但对它的更改不会影响父对象。

有没有解决这个问题的优雅方法?我对继承属性的奇特方式特别感兴趣。

【问题讨论】:

【参考方案1】:

您提出的两种方式都是好主意。让我再添加一个:元组!元组是不可变的。

@property
def bananas(self):
    return tuple(self._bananas)

既然您有了这些替代方案,那么在选择其中之一时需要牢记以下几点:

列表是否很小,您是否可以使用 O(n) 访问器?选择元组。在大多数情况下,消费者不会看到差异。 (当然,除非他试图改变它) 列表香蕉是否需要一些通用list 所缺乏的特殊能力?子类化列表并在变异函数上引发异常。 [1]

[1]: jsbueno 有一个 nice ReadOnlyList implementation 没有 O(n) 开销。

【讨论】:

【参考方案2】:

我花了很长时间,但我认为我已经根据answer 中提供的配方创建了一个非常强大且灵活的解决方案。我非常自豪地介绍了FixLen 包装器:

from array import array
from collections import MutableSequence
from inspect import getmembers

class Wrapper(type):
    __wraps__ = None
    __ignore__ = 
        '__class__', '__mro__', '__new__', '__init__', '__dir__',
        '__setattr__', '__getattr__', '__getattribute__',
    __hide__ = None

    def __init__(cls, name, bases, dict_):
        super().__init__(name, bases, dict_)
        def __init__(self, obj):
            if isinstance(obj, cls.__wraps__):
                self._obj = obj
                return
            raise TypeError(
                'wrapped obj must be of type '.format(cls.__wraps__))
        setattr(cls, '__init__', __init__)

        @property
        def obj(self):
            return self._obj
        setattr(cls, 'obj', obj)

        def __dir__(self):
            return list(set(dir(self.obj)) - set(cls.__hide__))
        setattr(cls, '__dir__', __dir__)

        def __getattr__(self, name):
            if name in cls.__hide__:
                return
            return getattr(self.obj, name)
        setattr(cls, '__getattr__', __getattr__)

        for name, _ in getmembers(cls.__wraps__, callable):
            if name not in cls.__ignore__ \
                    and name not in cls.__hide__ \
                    and name.startswith('__') \
                    and name not in dict_:
                cls.__add_method__(name)

    def __add_method__(cls, name):
        method_str = \
          'def method(self, *args, **kwargs):\n'              \
          '        return self.obj.method(*args, **kwargs)\n' \
          'setattr(cls, "method", method)'.format(method=name)
        exec(method_str)


class FixLen(metaclass=Wrapper):
    __wraps__ = MutableSequence   
    __hide__ = 
        '__delitem__', '__iadd__', 'append', 'clear', 'extend', 'insert',
        'pop', 'remove',
    

    # def _slice_size(self, slice):
    #     start, stop, stride = key.indices(len(self.obj))
    #     return (stop - start)//stride

    def __setitem__(self, key, value):
        if isinstance(key, int):
            return self.obj.__setitem__(key, value)
        #if self._slice_size(key) != len(value):
        if (lambda a, b, c: (b - a)//c)(*key.indices(len(self.obj))) \
          != len(value):
            raise ValueError('input sequences must have same length')
        return self.obj.__setitem__(key, value)

FixLen 保留对传递给其构造函数并阻止访问的可变序列的内部引用,或者提供更改对象长度的方法的替代定义。这允许我在内部改变长度,但在作为属性传递时保护序列的长度不被修改。这并不完美(我认为FixLen 应该是Sequence 的子类)。

示例用法:

>>> import fixlen
>>> x = [1,2,3,4,5]
>>> y = fixlen.FixLen(x)
>>> y
[1, 2, 3, 4, 5]
>>> y[1]
2
>>> y[1] = 100
>>> y
[1, 100, 3, 4, 5]
>>> x
[1, 100, 3, 4, 5]
>>> y.pop()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

【讨论】:

以上是关于如何使类属性不可变?的主要内容,如果未能解决你的问题,请参考以下文章

不可变类/对象、私有构造函数、工厂方法

不可变类特征

String为什么不可变?

String为什么不可变?

如何设置不可变文件属性

java中的不可变类型的探究