Is this Python Class mutable?

Viewed 123

Is this object mutable?

class Clock(object):
    def __init__(self, time):
        self.time = time
    def print_time(self):
        print(self.time)

boston_clock = Clock('5:30')
paris_clock = boston_clock
paris_clock.time = '10:30'
boston_clock.print_time()

It prints "10:30" even when I have expected to print "5:30" because strings are unmutable. Compared to code:

a = "Hello"
b = a
b += " World"
print(a)

it prints "Hello". The object Clock behaves as a list so I think it is mutable but I dont know why. And in that case, how can I make the Clock class unmutable?

2 Answers

Strings are indeed immutable, however, in your example, you are changing not a string variable, but an object's attribute. The attribute referrers to a value, so by re-assigning it, you just make it pointing to another value.

Python is a very dynamic language and it's hard to make something really protected from a sudden mutation. However, there is a convention to use _ at the beginning of an attribute name indicating that it's not supposed to be accessed and/or modified by any code outside of the class. And if you still want to have public time attribute, you can make it a property:

class Clock:
    def __init__(self, time):
        self._time = time

    @property
    def time(self):
        return self._time

    def print_time(self):
        print(self._time)

UPD: By the way, in Python variables are not so different from attributes. To access my_clock.time the interpreter needs to make some internal lookup of an attribute with the name time in a symbol table (just a fancy name for a dictionary) corresponding to the my_time object. At the same time, when you are accessing the local variable a, a similar lookup is being performed in another symbol table (aka dictionary) corresponding to the current local scope. Try out this snippet:

a = "Hello"
b = a
b += " World"
print(a)

locals()['a'] = "foobar"
print(a)

Even though strings are immutable, the second print outputs foobar.

If you really want to enforce something like your expected behavior you could use a getter/setter pattern:

class Clock(object):
    def __init__(self, time):
        self._time = time
    @property
    def time(self):
        print(self._time)
    @time.setter
    def time(self, value):
        raise AttributeError("Clock time does not support re-assignment")

Usage:

>>> c = Clock("5:30")
>>> c.time
5:30
>>> c.time = "10:30"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in time
AttributeError: Clock time does not support re-assignment

You could also just pass instead of raising an exception, but that would potentially come off to some as code "failing silently" when they otherwise expected the attribute to be changed.

That said: to @IvanVelichko's answer, the underlying _time attribute can still be reassigned to if the user wants to go out of their way to do that:

>>> c._time = "10:30"
>>> c.time
10:30

More broadly, the getter/setter pattern is used much less frequently in Python than something like Java for the reason shown above.

Related