Вопрос

Is there a way to return a mutable attribute but protect it from updates?

I have a Calendar class that uses an internal dict instance mapping datetime.date instances to list instances of event identifiers:

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

I would like to protect the calendar from these updates:

calendar = Calendar()
calendar.add_event(date, event)
event_ids = calendar.get_event_ids(date)
event_ids[0] = 42  # updates the mutable list in the calendar

The rationale is that a user might want to update the list of event identifiers returned by the calendar, but he does not expect that doing so will update the calendar.

A read-only property doesn’t prevent a user from updating the lists.

A solution seems to return copies of the lists. But:

  1. The list instances may become big and the Calendar.get_event_ids function may be called many times so performing a copy at each call can become inefficient.
  2. In case a user actually wants to change the calendar, returning a copy will mislead him to think he can, because no exception is raised from updates to the list instances.
  3. I think a user should be the one deciding to make a copy if he needs to do so.
  4. I want to know if there is any other way to achieve the same thing.

Another possibility would be to return instances of my own ImmutableList class. It solves 2 and 3, but not 1 (because I will have to copy the list instances to initialize ImmutableList instances) neither 4. Also, it seems like too much work for a very simple concern.

I also know that ‘We are all consenting adults’ and that overprotecting my instance attributes is not Pythonic. But I think my curiosity got the better of me.

Это было полезно?

Решение

Here is a way to create an immutable list which solves all your problems:

import collections.abc


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 exposes a tuple-like API so a user won’t accidentally change something, but has a private _list attribute which Calendar can access to manipulate the data. Note that the leading underscore is just a convention and doesn’ actually enforce private scope in the way that a language like C++ would. Here is a question which explains the scoping a bit better.

Другие советы

Another possibility would be to return instances of my own ImmutableList class. It solves 2 and 3, but not 1 (because I will have to copy the list instances to initialize ImmutableList instances) neither 4.

If you returned an immutable tuple instance from a mutable list instance it would perform a shallow copy (e.g. tuple([1, 2, 3])), but if you return your own ImmutableList instance you don’t have to perform any copy at initialization since you can store a reference to the mutable list instance. ImmutableList will be a protection proxy providing a read-only dynamic view on the mutable list instance (i.e. you cannot update the mutable list instance from the ImmutableList instance but changes to the mutable list instance will reflect in the ImmutableList instance):

import collections.abc
import datetime

class Event:
    def __init__(self, id_):
        self._id = id_
    def get_id(self):
        return self._id

class ImmutableList(collections.abc.Sequence):
    def __init__(self, list_):
        self._list = 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.get_id())
    def get_event_ids(self, date):
        return ImmutableList(self._dict[date])
>>> c = Calendar()
>>> d = datetime.date.today()
>>> c.add_event(d, Event('birthday'))
>>> l = c.get_event_ids(d)
>>> del l[0]  # read-only view
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'ImmutableList' object doesn't support item deletion
>>> list(l)
['birthday']
>>> c.add_event(d, Event('christmas'))
>>> list(l)  # dynamic view
['birthday', 'christmas']

Note that in this solution, the Calendar class stores and updates the mutable list instances directly and only wraps them in ImmutableList instances when returned from the Calendar.get_event_ids function. It differs from @aquavitae’s solution where the Calendar class stores and updates the mutable list instances from ImmutableList instances by accessing their internal attributes, thereby coupling the ImmutableList class with the Calendar class, which compromises information hiding (also known as encapsulation).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top