How can I make class properties immutable?

 ̄綄美尐妖づ 提交于 2021-02-19 06:52:06

问题


@property is a nice way to define getters. When the property is mutable, the reference returned can be used to modify the property in ways not controlled by the class definition. I'll use a banana stand as a motivating analogy, but this issue applies to any class that wraps a container.

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

I would like visitors to the banana stand to pay for their bananas. Unfortunately, there's nothing stopping a monkey (who doesn't know any better) from taking one of my bananas. The monkey didn't have to use the internal attribute _banana, they just took a banana without paying.

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

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

This analogy is a little silly, but any class that has mutable attributes is not protected from accidental vandalism. In my actual case, I have a class with two array attributes that must remain the same length. With array, there's nothing stopping a user from splicing a second array into the first and silently breaking my equal size invariant:

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

This same behavour occurs when the array is a property.

I can think of two ways of avoiding issue:

  1. Subclass array and override __setitem__. I am resistant to this because I would like to be able to use this array splicing behaviour internally.
  2. Change the accessor to return a deepcopy of the array. The returned array is still mutable, but changes to it won't affect the parent object.

Is there an elegant way around this problem? I'm particularly interested in fancy ways of subclassing property.


回答1:


The two ways you proposed are both good ideas. Let me throw in one more: tuples! Tuples are immutable.

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

Now that you have these alternative, there are a couple of things of things to keep in mind while choosing one over the other:

  • Is the list small and are you okay with a O(n) accessor? Choose tuple. For most part the consumer is not going to see a difference. (Unless of course he tries to mutate it)
  • Does the list bananas need some special abilities that a generic list is falling short on? Subclass a list and raise exceptions on mutating functions. [1]

[1]: jsbueno has a nice ReadOnlyList implementation that doesn't have the O(n) overhead.




回答2:


It took me a long time, but I think I've created a pretty robust and flexible solution based on the recipe provided in this answer. With great pride, I present the FixLen wrapper:

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 keeps an internal reference to the mutable sequence that you pass to its constructor and blocks access to, or provides an alternate definition of methods that change the length of the object. This allows me to mutate the length internally, but protect the length of the sequence from modification when passed as a property. It's not perfect (FixLen should subclass Sequence, I think).

Example usage:

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


来源:https://stackoverflow.com/questions/43486548/how-can-i-make-class-properties-immutable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!