如何使类属性不可变?

时间:2017-04-19 04:41:33

标签: python python-3.x properties decorator python-decorators

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

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

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

  1. 子类数组并覆盖__setitem__。我对此有抵触,因为我希望能够在内部使用这种数组拼接行为。
  2. 更改访问者以返回数组的深度复制。返回的数组仍然是可变的,但对它的更改不会影响父对象。
  3. 这个问题有优雅的方法吗?我特别感兴趣的是对子类化属性的奇特方式。

2 个答案:

答案 0 :(得分:2)

你提出的两种方式都是好主意。让我再说一遍:元组!元组是不可变的。

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

既然您有这些替代方案,那么在选择其中一项时要记住几件事情:

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

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

答案 1 :(得分:1)

我花了很长时间,但我认为我已根据此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