One solution here is to provide a read-only "view" of your _items
attribute. This is actually something that Python does by default with class dictionaries.
If I have a class Foo
, like so:
class Foo:
pass
f = Foo()
then you'll find that the instance dictionary of f
(where the attributes for that instance are stored) and the class dictionary of Foo
(where the attributes of the class are stored) are of different types.
>>> f.__dict__
{}
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
What's a mappingproxy
object? Well, it's a readonly, dynamically updated, view of a dictionary. If we add an attribute to to Foo
, the mappingproxy
object is updated, but we can't mutate the mappingproxy
object directly:
>>> cls_dict = Foo.__dict__
>>> 'bar' in cls_dict
False
>>> Foo.bar = 'baz'
>>> 'bar' in cls_dict
True
>>> cls_dict['baz'] = 'spam'
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
You can create your own mappingproxy
objects yourself by importing MappingProxyType
from the types
module:
>>> from types import MappingProxyType
>>> mydict = {}
>>> mydict_proxy = MappingProxyType(mydict)
>>> mydict['spam'] = 'bar'
>>> mydict_proxy
mappingproxy({'spam': 'bar'})
>>> mydict_proxy['baz'] = 'foo'
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
Sadly, there isn't an equivalent SequenceProxy
type in the Python standard library, which in my opinion would be useful for cases like this. However, it's fairly trivial to implement a SequenceProxy
class, which means you could refactor your Library
class like so:
import sys
import collections.abc
from typing import TypeVar, Sequence, Generic, Iterator, Union, Any
T = TypeVar('T')
S = TypeVar('S', bound='SequenceProxy[Any]')
@collections.abc.Sequence.register
class SequenceProxy(Generic[T]):
"""Read-only proxy for a sequence.
Similar in concept to `MappingProxyType`, but for sequences rather than mappings.
"""
__slots__ = '_sequence',
def __init__(self, initsequence: Sequence[T]) -> None:
self._sequence = initsequence
def __iter__(self) -> Iterator[T]:
return iter(self._sequence)
def __getitem__(self: S, index: Union[int, slice]) -> Union[T, S]:
if isinstance(index, slice):
return self.__class__(self._sequence[index])
return self._sequence[index]
def __len__(self) -> int:
return len(self._sequence)
def __contains__(self, item: Any) -> bool:
return item in self._sequence
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self._sequence!r})'
def __str__(self) -> str:
return str(self._sequence)
def __reversed__(self) -> Iterator[T]:
return reversed(self._sequence)
def index(self, value: Any, start: int = 0, stop: int = sys.maxsize) -> int:
"""Return the index of a value in the underlying sequence."""
return self._sequence.index(value, start, stop)
def count(self, value: Any) -> int:
"""Return the number of times a value appears in the sequence."""
return self._sequence.count(value)
class Library:
def __init__(self, name) -> None:
self._hours = []
items = []
self._items = items
self._items_proxy = SequenceProxy(items)
self._name = name
def get_hours_str(self):
ret = ""
for day in self._hours:
ret += day + '\n'
return ret
def get_items(self):
return self._items_proxy
However, you should note that it is generally considered more pythonic to use properties rather than "getter and setter" methods, meaning instead of your get_items
method, you might do this:
class Library:
def __init__(self, name) -> None:
self._hours = []
items = []
self._items = items
self._items_proxy = SequenceProxy(items)
self._name = name
def get_hours_str(self):
ret = ""
for day in self._hours:
ret += day + '\n'
return ret
@property
def items(self):
return self._items_proxy
You can read more about the @property
decorator -- how it works, and when to use it -- here.