3

I am working in Python so attribute privacy isn't enforced. I have a Library class that stores a list of library Items related to that library. I want to be able to access the library Items through the Library class. Right now I accomplish that by returning the "private" items attribute. However, any class would then be able to modify that list. Would I need to make a "deep copy" of these in order for the items attribiute to be truly private? Should I just make the attribute public?

class Library():
    def __init__(self, name) -> None:
        self._hours = []
        self._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
3
  • 3
    It really depends what you are trying to accomplish. I would say it is definitely suspicious though. Why do you want access to this list? Perhaps you can provide something weaker that allows you to do what you want and without the cost of a copy. For instance, if you just want to iterate through the list, provide an iterator of the list rather than the list itself. Commented Aug 28, 2021 at 16:18
  • Thanks for your input @JustSomeGuy. I will be iterating through the items so an iterator makes sense.
    – Cherise
    Commented Aug 28, 2021 at 16:39
  • 2
    If you’re looking to protect your _items array from becoming aliased and received unintended external modifications, you can use a wrapper object which exposes a read-only view of the array’s contents, but without copying it. This looks promising: pypi.org/project/immutable-views The tricky part is how to handle nested structures. E.g. if you have an array of dictionaries, and you make an immutable view of the array, and look up a dictionary, I don’t think it’ll wrap the dict in an immutable view automatically, which is an issue.
    – Alexander
    Commented Sep 1, 2021 at 12:24

4 Answers 4

4

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.

8
  • 1
    I love your answer. Minor nitpick: it would be better to use sys.maxsize instead of hardcoding a specific value as the default for the stop argument to index.
    – Jasmijn
    Commented Sep 1, 2021 at 14:09
  • 1
    That's pretty nice. Ever considered suggesting it as part of the standard library? It doesn't seem like much of a stretch given the existence of MappingProxyType.
    – JimmyJames
    Commented Sep 1, 2021 at 14:44
  • 2
    I think there's a good use case for it. One of the weaknesses of Python IMO is its limited support for immutability. I think this is especially true as Python's use cases continue to expand. C versus pure? I feel like we shouldn't care either way but maybe I'm missing something. I'm not familiar with MappingProxyType and it's purpose but it seems odd that it's in types to me. I don't think you'd really make that decision on your own though.
    – JimmyJames
    Commented Sep 1, 2021 at 15:19
  • 1
    @JimmyJames I've taken the plunge discuss.python.org/t/feature-proposal-a-sequenceproxy-class/… Commented Sep 1, 2021 at 17:18
  • 1
    @AlexWaygood If it's not accepted for some reason, this could be the seed of a decent library. You could provide a decorator for these kinds of methods. On that line of thinking, I searched for "proxy decorator python" and found this but I'm not sure it's designed around the immutability use case.
    – JimmyJames
    Commented Sep 1, 2021 at 17:48
4

I guess you are mixing up two concepts here:

  • privacy of members of a class

  • immutability of a class

In the code shown in the question, self._items is "truly private" in the usual Python sense of "privacy by convention", there is no method in Library which allows to change self._items. However, you correctly observed that the current implementation of get_items allows the caller of that method to modify the list itself - hence Library is not an immutable class any more.

Now in case you want that class to be immutable, you should change get_items either to return

  • a full deep copy of the list (as you wrote by yourself)

  • an iterator, as someone mentioned in the comments (examples are here)

Alternatively, one could also replace get_items by an index-based get_item(self, index) method, together with a get_itemcount(self) method.

For getting more information about when to make classes immutable or not, have a look into this older SE.SE question.

4
  • "Alternatively, one could also replace get_items by an index-based get_item(self, index) method, together with a get_itemcount(self) method." You usually don't want to do this, it is both less flexible and less convenient than using an iterator.
    – Jasmijn
    Commented Sep 1, 2021 at 14:02
  • @Jasmijn: not necessarily, since it allows efficient access in random order without making a full copy, and there are scenarios where this can matter. For example, imagine there are 100000 library items, but your use case requires to pick only a few of them by their index. Try this with an iterator or a full copy of the list and compare the performance.
    – Doc Brown
    Commented Sep 1, 2021 at 14:30
  • 1
    If that is a relevant use case, you're still better off returning a view object, which provides both iteration as well as random access.
    – Jasmijn
    Commented Sep 1, 2021 at 14:36
  • @Jasmijn I think you trailed off: '... and indexed access'. Also you can use such an object with for and in etc. instead of having to explicitly code around these custom method names.
    – JimmyJames
    Commented Sep 1, 2021 at 14:41
2

This is a semantical difference on what you mean by "X can modify Y".

If myField is private, but SetMyField(newValue) is public, you can argue whether or not another object is modifying the private field. Strictly speaking, it is not modifying the field. It is calling a function, and that function is the one modifying the private field.

I used setting as an example here, but the same principle applies to getting.

In other words, from a strict reading, "X can modify Y" is not equivalent to "X can call on Z to modify Y", but your question seems to equate the two.

This distinction matters, because "private" does not mean "inaccessible". It means "only accessible to the current class". Therefore, the decision to modify or return the value can still be made, but only inside that class. An outside actor cannot independently decide to modify/access the private field without its class explicitly providing a way for it to be modified/accessed.

Would I need to make a "deep copy" of these in order for the items attribiute to be truly private?

While reference vs value types definitely influence the impact of what can happen to a returned value (i.e. a reference type object can be modified after only returning the reference to the object), this doesn't quite define the reasoning behind public and private access on a rudimentary level.

Should I just make the attribute public?

This is arguable.

I'm no Python dev, so I'm not sure if you can make something publically gettable yet only privately settable. Assuming you can, and assuming that you never intend to alter the value when it is being retrieved, then there is little issue with making such a field publically gettable.

However, "never" is a big claim. Can you really account for all possible derived classes or future requirements? If you can, then what I said applies. But if you want to be careful about this, it's better to pass it through a getter method so that you retain the option of easily making alterations if or when you want to.

0

Based on your question, I'm a little unsure why you would need a 'deep copy' in which not only is a new list created, each object in the list would need to be cloned as well. If you don't want people to inadvertently modify the underlying list, a shallow copy i.e. list.copy() should be sufficient. Only in the case where you are concerned that someone might modify the objects within the list that you should would need a 'deep copy'. More information is available here on the difference between the two.

So you need to determine whether it's the structure of the list that is of concern or the objects within the list as well.

Also, one other option to consider in your methods is using a generator:

def get_items(self):
    for item in self._items:
        yield item

On a side note, from a style perspective, prefixing get is a Java convention but I personally prefer plain def items(self) to return self._items here.

4
  • Thanks for your input. I'm concerned about the items in the list and the structure of the list (order doesn't matter but presence does).
    – Cherise
    Commented Sep 2, 2021 at 23:25
  • 1
    @Cherise Are you concerned about which items are in the list or the state of those items. There's a subtle distinction there that determines whether you really need a deep copy or a shallow copy. For example, if your list is full of ints, a shallow copy should be fine because no one can change the value of say 0 to 1. On the other hand, if this is a list of lists, a shallow copy would only protect the top level list and you would need a deep copy.
    – JimmyJames
    Commented Sep 3, 2021 at 15:01
  • The state of the items within the list. Looks like a deep copy will do for now and I'll try to learn a bit more about the proxy solution later.
    – Cherise
    Commented Sep 3, 2021 at 16:20
  • 1
    @Cherise The main concern I would have with deepcopy() is performance. If your list is large, this could be problematic. I don't think the proxy solution will protect the item state either, though. You'd need to proxy each of the objects in the list as well.
    – JimmyJames
    Commented Sep 3, 2021 at 16:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.