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 "", line 1, in 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######################################################################## File "c:\Users\mdriscoll\Desktop\rep-fonts\immutable\immute_slots.py", line 33, in __setattr__ raise AttributeError(msg) AttributeError: ' ' 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:
- Python documentation on slots
- StackOverflow: python __slots__
- What the H*ll is a slot?
https://stackoverflow.com/questions/472000/python-slots
The `__setattr__` is unnecessary. You can still use `i.one = 1`.
Thanks for the feedback. I had tried several different approaches and I was getting some weird behavior when I tried to do it that way. Of course, now that I try again it works in the manner you mention.
You’re missing the point of immutability. As wikipedia states, “an immutable object is an object whose state cannot be modified after it is created”. In this case, Immutable objects can still be modified, for instance:
p = Immutable(1,2,3)
p.one = 2
print p.one # Prints 2
A truly immutable object wouldn’t allow that. To achieve it, I’d override __setattr__ like this:
def __setattr__(self, name, value):
pass # Or raise an exception
Yes, you’re right. I did override __setattr__ originally and then removed that from the example. I just re-added it and added a little explanation about the change. Thanks for that!
No matter how you implement your class with __slots__ it will still be mutable.
>>> p = Immutable()
>>> object.__setattr__(p, “one”, 2)
>>> p.one
2
Pingback: What is the Pythonic way to make an immutable class? – FeuTex – #ForAuthors