Why Is 'Self' in Python Objects Immutable

Why is `self` in Python objects immutable?

Any simple assignment to any argument of any function behaves exactly the same way in Python: binds that name to a different value, and does nothing else whatsoever. "No special case is special enough to break the rules", as the Zen of Python says!-)

So, far from it being odd (that simply=assigning to a specific argument in a specific function has no externally visible effect whatsoever), it would be utterly astonishing if this specific case worked in any other way, just because of the names of the function and argument in question.

Should you ever want to make a class that constructs an object of a different type than itself, such behavior is of course quite possible -- but it's obtained by overriding the special method __new__, not __init__:

class Test(object):
def __new__(cls):
return 5

t = Test()
print t

This does emit 5. The __new__ / __init__ behavior in Python is an example of the "two-step construction" design pattern: the "constructor" proper is __new__ (it builds and returns a (normally uninitialized) object (normally a new one of the type/class in question); __init__ is the "initializer" which properly initializes the new object.

This allows, for example, the construction of objects that are immutable once constructed: in this case everything must be done in __new__, before the immutable object is constructed, since, given that the object is immutable, __init__ cannot mutate it in order to initialize it.

Why do Python immutable types (like int, str, or tuple) need to use `__new__()` instead of just `__init__()`?

I'm the question OP and I'm going to answer my own question because I think I found out the answer half-way through typing it. I'm not going to mark it as correct until others have confirmed it to be correct.

This question here is particularly relevant, but the question wasn't the same as this question, and although the answer was very enlightening (though the comments turned into enlightening but esoteric arguments about C and Python and "pythonic"), it should be set out more clearly here to specifically address this question. I hope this will help future readers. The code in this answer has been verified in Python 3.6.1.

The thing about an immutable object, is that you don't want to set its members once it's been created, obviously. The way you do that in Python is to override the __setattr__() special method to raise an error (AttributeError), so that people can't do things like my_immutable_object.x = 3. Take the following custom immutable class for example.

class Immutable(object):
def __init__(self, a, b):
self.a = a
self.b = b

def __setattr__(self, key, value):
raise AttributeError("LOL nope.")

Let's try using it.

im = Immutable(2, 3)
print(im.a, im.b, sep=", ")

Output:

AttributeError: LOL nope.

"But what!?", I hear you ask, "I didn't set any of its attributes after it's been created!" Ah but yes you did, in the __init__(). Since __init__() is called after the object is created, the lines self.a = a and self.b = b are setting the attributes a and b after the creation of im. What you really want is to set the attributes a and b before the immutable object is created. An obvious way to do that is to create a mutable type first (whose attributes you are allowed to set in __init__()), and then make the immutable type a subclass of it, and make sure you implement the __new__() method of the immutable child class to construct a mutable version first, and then make it immutable, like the following.

class Mutable(object):
def __init__(self, a, b):
self.a = a
self.b = b

class ActuallyImmutable(Mutable):
def __new__(cls, a, b):
thing = Mutable(a, b)
thing.__class__ = cls
return thing

def __setattr__(self, key, value):
raise AttributeError("LOL nope srsly.")

Now let's try running it.

im = ActuallyImmutable(2, 3)
print(im.a, im.b, sep=", ")

Output:

AttributeError: LOL nope srsly.

"WTF!? When did __setattr__() get called this time?" The thing is, ActuallyImmutable is a subclass of Mutable, and without explicitly implementing its __init__(), the parent class's __init__() is automatically called after the creation of the ActuallyImmutable object, so in total the parent's __init__() is called twice, once before the creation of im (which is OK) and once after (which is not OK). So let's try again, this time overriding AcutallyImmutable.__init__().

class Mutable(object):
def __init__(self, a, b):
print("Mutable.__init__() called.")
self.a = a
self.b = b

class ActuallyImmutable(Mutable):
def __new__(cls, a, b):
thing = Mutable(a, b)
thing.__class__ = cls
return thing

# noinspection PyMissingConstructor
def __init__(self, *args, **kwargs):
# Do nothing, to prevent it from calling parent's __init__().
pass

def __setattr__(self, key, value):
raise AttributeError("LOL nope srsly.")

Now it should work.

im = ActuallyImmutable(2, 3)
print(im.a, im.b, sep=", ")

Output:

2, 3

Good, it worked. Oh, don't worry about the # noinspection PyMissingConstructor, that's just a PyCharm hack to stop PyCharm from complaining that I didn't call the parent's __init__(), which obviously is what we intend here. And finally just to check that im really is immutable, verify that im.a = 42 will give you AttributeError: LOL nope srsly..

How to make a specific instance of a class immutable?

You can't make Python classes fully immutable. You can however imitate it:

class C:
_immutable = False
def __setattr__(self, name, value):
if self._immutable:
raise TypeError(f"Can't set attribute, {self!r} is immutable.")
super().__setattr__(name, value)

Example:

>>> c = C()
>>> c.hello = 123
>>> c.hello
123
>>> c._immutable = True
>>> c.hello = 456
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in __setattr__
TypeError: Can't set attribute, <__main__.C object at 0x000002087C679D20> is immutable.

If you wish to set it at initialization, you can add an __init__ like so:

class C:
_immutable = False
def __init__(self, immutable=False):
self._immutable = immutable
def __setattr__(self, name, value):
if self._immutable:
raise TypeError(f"Can't set attribute, {self!r} is immutable.")
super().__setattr__(name, value)

Keep in mind you can still bypass it by accessing and modifying the __dict__ of the instance directly:

>>> c = C(immutable=True)
>>> c.__dict__["hello"] = 123
>>> c.hello
123

You may attempt to block it like so:

class C:
_immutable = False
def __init__(self, immutable=False):
self._immutable = immutable
def __getattribute__(self, name):
if name == "__dict__":
raise TypeError("Can't access class dict.")
return super().__getattribute__(name)
def __setattr__(self, name, value):
if self._immutable:
raise TypeError(f"Can't set attribute, {self!r} is immutable.")
super().__setattr__(name, value)

But even then it's possible to bypass:

>>> c = C(immutable=True)
>>> object.__getattribute__(c, "__dict__")["hello"] = 123
>>> c.hello
123

How to make an immutable object in Python?

Using a Frozen Dataclass

For Python 3.7+ you can use a Data Class with a frozen=True option, which is a very pythonic and maintainable way to do what you want.

It would look something like that:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
a: Any
b: Any

As type hinting is required for dataclasses' fields, I have used Any from the typing module.

Reasons NOT to use a Namedtuple

Before Python 3.7 it was frequent to see namedtuples being used as immutable objects. It can be tricky in many ways, one of them is that the __eq__ method between namedtuples does not consider the objects' classes. For example:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2 # will be True

As you see, even if the types of obj1 and obj2 are different, even if their fields' names are different, obj1 == obj2 still gives True. That's because the __eq__ method used is the tuple's one, which compares only the values of the fields given their positions. That can be a huge source of errors, specially if you are subclassing these classes.

Python: self vs type(self) and the proper use of class variables

After speaking with others offline (and per @wwii's comment on one of the answers here), it turns out the best way to do this without embedding the class name explicitly is to use self.__class__.attribute.

(While some people out there use type(self).attribute it causes other problems.)

Python mutable class variable vs immutable class variable

self.i += 1

is equivalent to

self.i = self.i + 1

When the instance variable does not exist, the value is looked up on the class, so in this scenario, it is equivalent to

self.i = S.i + 1

After you define self.i, then any further value lookup is on the instance variable, not on the class variable. So after this line, you have S.i = 0 and s1.i = 1. Since S.i is not modified, s2.i also becomes 1.

On the other hand,

self.a.append(1)

does not create a new instance variable, but appends an element to the existing class variable.



Related Topics



Leave a reply



Submit