【问题标题】:How to return a mutable attribute as an immutable object?如何将可变属性作为​​不可变对象返回?
【发布时间】:2014-02-20 20:17:30
【问题描述】:

有没有办法返回一个可变属性但保护它不被修改?我会更具体:

我有一个 Calendar 类,它使用内部字典映射 datetime.date 对象到 int 事件 ID 的 list 对象:

class Calendar:

    def __init__(self):
        self._dict = {}
        
    def add_event(self, date, event):
        if date in self._dict:
            self._dict[date].append(event.id)
        else:
            self._dict[date] = [event.id]
            
    def get_event_ids(self, date):
        if date in self._dict:
            return self._dict[date]
        else:
            return []

如您所见,它非常简单。我只想封装列表创建和事件 ID 提取逻辑。但我想保护这个日历不被无意修改。更具体地说,不知情的用户实际上可以这样做:

calendar = Calendar()
calendar.add_event(some_date, some_event)
event_ids = calendar.get_event_ids(some_date)
event_ids[0] = 42  # modifies the internal list in the calendar

我知道这个例子看起来有点特别,但如果你仔细想想,用户可能想以某种方式修改日历返回的列表,为什么他希望这样做会修改里面的日历?

另一件(类似的)事情是当我想设置一个只读属性时。 Pythonic 方法是使用 property 而不使用 setter 方法,但如果属性是可变的,则根本无法保护您免受外部修改。

显而易见的答案似乎是“好吧,你为什么不返回列表的副本?”但是:

  1. 列表可能会变得太大,而且这个方法会被调用很多次。每次复制列表可能会有点慢。
  2. 如果用户确实想要更改日历,返回副本会误导 h(im/er) 认为他们可以,因为列表的变化不会引发异常。
  3. 我认为,如果需要,用户应该花时间/精力来制作副本。
  4. 我想知道是否有其他方法可以实现相同的目标。

另一种可能性是制作我自己的ImmutableList 并返回它。它解决了 2 和 3,但不是 1(因为我必须将内部可变列表复制到 ImmutableList)也不是 4。此外,对于一个非常简单的问题来说,这似乎工作量太大。

我也知道“我们都是成年人”,过度保护我的实例属性是不合常规的。但我认为我的好奇心战胜了我。

【问题讨论】:

  • 你可以返回迭代器而不是列表本身。
  • 如果你想要一个不可变的集合,那么使用一个不可变的连接(例如tuple)。只需将get_event_ids 修改为return tuple(self._dict[date])。顺便说一句:你真的应该在你的情况下使用defaultdict(list) 并避免,这可以让你避免所有ifs。
  • 你的英语绝对高于平均水平,恕我直言。 :)
  • @Bakuriu 但是从列表中创建一个新元组并不比复制它花费相同(或更多)的时间?无论如何,感谢您的defaultdict 提示!
  • @Laski 这就是tuple 所做的。构建一个列表需要相同的时间,但它是不可变的。避免复制的唯一方法是使用视图(就像您接受的答案一样,尽管我相信它可以改进。例如最好使用defaultdict(list),修改PrivateList,使其接受@987654341 @ in __init__ 并将get_event_ids 更改为返回PrivateList(self._dict[date])。这样可以减少创建大量视图和访问私有属性的开销。

标签: python collections immutability


【解决方案1】:

这是一种创建不可变列表的方法,可以解决您的所有问题:

import collections


class ImmutableList(collections.abc.Sequence):

    def __init__(self):
        self._list = []

    def __getitem__(self, index):
        return self._list[index]

    def __len__(self):
        return len(self._list)


class Calendar:

    def __init__(self):
        self._dict = collections.defaultdict(ImmutableList)

    def add_event(self, date, event):
        self._dict[date]._list.append(event.id)

    def get_event_ids(self, date):
        return self._dict[date]

ImmutableList 公开了一个类似元组的 API,因此用户不会意外更改某些内容,但有一个私有的 _list 属性,Calendar 可以访问该属性来操作数据。请注意,前导下划线只是一个约定,实际上并没有像 C++ 这样的语言那样强制执行私有作用域。 Here 是一个更好地解释范围的问题。

【讨论】:

    【解决方案2】:

    另一种可能性是制作我自己的ImmutableList 并返回它。它解决了 2 和 3,但不是 1(因为我必须将内部可变列表复制到 ImmutableList)也不是 4。

    从list 初始化tuple 会执行list 的浅拷贝,但初始化您自己的ImmutableList 不需要list 的任何副本。这是protection proxy 的示例,其中ImmutableList 控制对Calendar 的内部可变list 的访问(只读):

    import collections
    
    
    class ImmutableList(collections.abc.Sequence):
    
        def __init__(self, mutable_list):
            self._list = mutable_list
    
        def __getitem__(self, index):
            return self._list[index]
    
        def __len__(self):
            return len(self._list)
    
    
    class Calendar:
    
        def __init__(self):
            self._dict = collections.defaultdict(list)
    
        def add_event(self, date, event):
            self._dict[date].append(event.id)
    
        def get_event_ids(self, date):
            return ImmutableList(self._dict[date])
    

    此解决方案与 @aquavitae’s 略有不同,Calendar 存储 ImmutableList 并通过访问其内部可变列表来改变它们,妥协 information hiding(也称为封装)并将 ImmutableList 与 @987654338 耦合@。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-07-02
      • 1970-01-01
      • 1970-01-01
      • 2010-11-21
      • 1970-01-01
      • 2021-03-20
      相关资源
      最近更新 更多