I’ve been reading a lot about Python’s magic methods lately and recently read about a couple of ways to create an immutable class. An immutable class does not allow the programmer to add attributes to an instance (i.e. monkey patch). It’s a little easier to understand if we actually look at a normal class first. We’ll start with a monkey patching example and then look at a way to make the class “immutable”.

Monkey Patching Python Classes

First of all, we need to create a class that we can play with. Here’s a simple class that doesn’t do much of anything:

########################################################################
class Mutable(object):
    """
    A mutable class
    """
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        pass

Now let’s create an instance of this class and see if we can add an attribute:

>>> mut_obj = Mutable()
>>> mut_obj.monkey = "tamarin"
>>> mut_obj.monkey
'tamarin'

This class does allow us to add attributes to it at run time. Now that we know how to do some simple monkey patching, let’s try to block that behavior.

Creating an Immutable Class

One of the examples I was reading about immutable classes mentioned that you could create one by replacing a class’s __dict__ with __slots__. Let’s see how that looks:

########################################################################
class Immutable(object):
    """
    An immutable class
    """
    __slots__ = ["one", "two", "three"]
 
    #----------------------------------------------------------------------
    def __init__(self, one, two, three):
        """Constructor"""
        super(Immutable, self).__setattr__("one", one)
        super(Immutable, self).__setattr__("two", two)
        super(Immutable, self).__setattr__("three", three)
 
    #----------------------------------------------------------------------
    def __setattr__(self, name, value):
        """"""
        msg = "'%s' has no attribute %s" % (self.__class__,
                                            name)
        raise AttributeError(msg)

Now we just need to create an instance of this class to see if we can monkey patch it:

>>> i = Immutable(1, 2, 3)
>>> i.four = 4
Traceback (most recent call last):
  File "<string>", line 1, in <fragment>
AttributeError: 'Immutable' object has no attribute 'four'

In this case, the class does not allow us to monkey patch the instance. Instead, we receive an AttibuteError. Let’s try to change one of the attributes:

>>> i = Immutable(1, 2, 3)
>>> i.one = 2
Traceback (most recent call last):
  File "c:\Users\mdriscoll\Desktop\rep-fonts\immutable\immute_slots.py", line 1, in <module>
    ########################################################################
  File "c:\Users\mdriscoll\Desktop\rep-fonts\immutable\immute_slots.py", line 33, in __setattr__
    raise AttributeError(msg)
AttributeError: '<class '__main__.Immutable'>' has no attribute one

This is because we have overridden the __setattr__ method. You could just override the method and not do anything at all if you wanted. This would stop the traceback from happening, but also prevent the value from being changed. If you like to be explicit with what is going on, raising an error is probably the way to go.

If you do any reading about slots, you will quickly find that using slots in this manner is discouraged. Why? Because slots were created primarily as a memory optimization (it reduces attribute access time).

You can read more about slots at the following links:

https://stackoverflow.com/questions/472000/python-slots

Print Friendly