2017-04-19 64 views
1

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

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. 更改访问器以返回数组的深度拷贝。返回的数组仍然是可变的,但对其的更改不会影响父对象。

是否有解决此问题的优雅方法?我特别感兴趣的是分类财产的奇妙方式。

回答

2

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

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

现在,你有这些选择,有一对夫妇的事情的事情要记住,而选择了另一种:

  • 是List小,你还好吗用Ø (n)访问者?选择元组。大部分消费者不会看到差异。 (当然,除非他试图改变它)
  • 列表香蕉是否需要一些特殊的能力,通用list是不足之处?子类化一个列表并在变异函数上引发异常。 [1]

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

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