2

For example, I have a class with a field __x, which is a list:

class C():
    def __init__(self, xx):
        self.__x = xx
    @property
    def x(self):
        return self.__x
    @x.setter
    def x(self, xx):
        raise Exception("Attempt to change an immutable field")

I can prevent changes such as these:

c = C([1,2,3])
c.x = [3,2,1]

But how can I prevent a change such as this?

c.x.append(4)
10
  • 1
    You can't, really. Trying to make things immutable is pretty tough in Python, unless you want to write a C-extension. Here, list objects are always mutable, you could define an "immutable" wrapper perhaps. Note, you can simply just not define a setter and it will throw an error Commented Nov 21, 2018 at 22:06
  • @juanpa.arrivillaga Is it OK to use tuples inside my class then? I will justuse list(self.__x) every time I want it to be list. Or it's a bad practice? Commented Nov 21, 2018 at 22:12
  • How about converting the list to a tuple? Commented Nov 21, 2018 at 22:12
  • 1
    You can, of course, just return a copy of your list as well, and then .append simply won't matter Commented Nov 21, 2018 at 22:12
  • 1
    @ttt well then use a list and return a copy of it, or a tuple if you'd prefer, but a copy would work self._x.copy() Commented Nov 21, 2018 at 22:21

2 Answers 2

2

In the final analysis, you cannot protect your objects from inspection and manipulation. Also, always ask yourself "from whom, exactly?" when you want to "protect" data.

Sometimes it's just not worth the effort to code around users not reading the documentation.

That being said, you could consider return tuple(self.__x) in the getter. On the other hand, if __x contains other mutable objects, that would not prevent a user from manipulating those inner objects. (return list(self.__x) would also return a shallow copy of the data, but with less implicit "hey, I'm supposed to be immutable!" signaling.)

Something you should definitely consider is to change self.__x = xx to self.__x = list(xx) in the __init__ method, such that users doing

var = []
c = C(var)

can't "easily" (or by mistake, and again, there could be mutable inner objects) change the state of c by mutating var.

Sign up to request clarification or add additional context in comments.

2 Comments

(There's also copy.deepcopy, but not worth the effort and expensive if called with every attribute access.)
@juanpa.arrivillaga not for lists with mutable inner objects, such as lists of lists.
0

The simplest approach would be to accept an iterable on __init__ and turn it to a tuple internally:

class C(object):
    def __init__(self, iterable):
        self._tuple = tuple(iterable)

    @property
    def x(self):
        return self._tuple

    @x.setter
    def x(self, value):
      raise RuntimeError('can\'t reset the x attribute.')

c = C([1, 2, 3])
# c.x = 'a' Will raise 'RuntimeError: can't reset the x attribute.'
print(c.x)

A design like this one makes any object instantiated from the class immutable, so that mutating operations should return new objects instead of changing the state of the current one.

Let's say for instance that you want to implement a function that increment by one each item in self.x. With this approach you need to write something similar to:

def increment_by_one(c):
  return C(t+1 for t in c.x)

As there's a cost associated with creating and destroying objects the trade-offs between this approach (which prevents mutation of the x attribute) and the one suggested by @timgeb should be evaluated on your use-case.

2 Comments

Sure, but C could rely on _tuple being mutable internally.
@timgeb Not a request I inferred from the OP. A class like that (immutable) probably makes sense if a mutating operation returns a different instance.

Your Answer

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