如何使类属性不可变?

cas*_*avo 4 python properties decorator python-3.x 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
Run Code Online (Sandbox Code Playgroud)

我希望香蕉摊的游客为他们的香蕉买单。不幸的是,没有什么能阻止一只猴子(谁也不知道更好)拿走我的一根香蕉。猴子不必使用内在属性_banana,他们只是不付钱就拿了一根香蕉。

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

>>> stand = BananaStand()
>>> stand.bananas
['b1', 'b2']
>>> take_banana(stand)
'b2'
>>> stand.bananas
['b1']
Run Code Online (Sandbox Code Playgroud)

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

>>> 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])
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Ult*_*nct 6

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

@property
def bananas(self):
    return tuple(self._bananas)
Run Code Online (Sandbox Code Playgroud)

既然您有了这些选择,那么在选择其中一个时需要记住以下几点:

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

[1]:jsbueno 有一个很好的ReadOnlyList实现,它没有 O(n) 开销。