6

As per my understanding Python user defined class instances are by default immutable. Immutable objects does not change their hash value and they can be used as dictionary keys and set elements.

I have below code snippet.

class Person(object):
 def __init__(self, name, age):
 self.name=name
 self.age=age

Now, I will instantiate Person class and create an object and print its hash value.

jane = Person('Jane', 29)
print(jane.__hash__())
-9223371933914849101

Now, I will mutate jane object and print its hash value.

jane.age = 33
print(jane.__hash__())
-9223371933914849101

My question is even if jane object is mutable why its hash value is not changing?

Also, I can use mutable jane object as dict key and set element.

asked Feb 25, 2017 at 6:49
3
  • 4
    "As per my understanding Python user defined class instances are by default immutable" - on the contrary, instances of user-defined classes are mutable by default, and trying to make them immutable is quite a mess. Commented Feb 25, 2017 at 7:23
  • @user2357112 one can monkey-patch a class, so I am quite sure class instances are mutable. See stackoverflow.com/questions/5626193/what-is-a-monkey-patch Commented Feb 25, 2017 at 7:26
  • @Mai: You can monkey-patch classes, sure, but whether that counts as mutating their instances is up for debate. In any case, you can get mostly-un-monkey-patchable classes by writing them with Cython or using the C API directly, and you can get mostly-immutable instances even without bringing C into the picture by inheriting from a built-in class with immutable instances and setting __slots__ = () to disable instance __dict__ creation. Commented Feb 25, 2017 at 8:02

7 Answers 7

11

To define a class with immutable instances, you can do something like this:

class Person:
 """Immutable person class"""
 # Using __slots__ reduces memory usage.
 __slots__ = ('name', 'age')
 def __init__(self, name, age):
 """Create a Person instance.
 Arguments:
 name (str): Name of the person.
 age: Age of the person.
 """
 # Parameter validation. This shows how to do this,
 # but you don't always want to be this inflexibe.
 if not isinstance(name, str):
 raise ValueError("'name' must be a string")
 # Use super to set around __setattr__ definition
 super(Person, self).__setattr__('name', name)
 super(Person, self).__setattr__('age', int(age))
 def __setattr__(self, name, value):
 """Prevent modification of attributes."""
 raise AttributeError('Persons cannot be modified')
 def __repr__(self):
 """Create a string representation of the Person.
 You should always have at least __repr__ or __str__
 for interactive use.
 """
 template = "<Person(name='{}', age={})>"
 return template.format(self.name, self.age)

A test:

In [2]: test = Person('S. Eggs', '42')
In [3]: str(test)
Out[3]: "<Person(name='S. Eggs', age=42)>"
In [4]: test.name
Out[4]: 'S. Eggs'
In [5]: test.age
Out[5]: 42
In [6]: test.name = 'foo'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-1d0482a5f50c> in <module>()
----> 1 test.name = 'foo'
<ipython-input-1-efe979350b7b> in __setattr__(self, name, value)
 24 def __setattr__(self, name, value):
 25 """Prevent modification of attributes."""
---> 26 raise AttributeError('Persons cannot be modified')
 27 
 28 def __repr__(self):
AttributeError: Persons cannot be modified
answered Jan 2, 2018 at 4:09
4
  • Why would __slots__ be undesired when subclassing? Slots are inherited from a parent, and can be expanded upon within a child class. I might be missing something though. Commented Feb 2, 2023 at 13:31
  • @ChaddRobertson Have you tried it? You will get AttributeError: Persons cannot be modified. Commented Feb 2, 2023 at 20:42
  • That's not as a result of using __slots__ though, that's due to the overridden __setattr__ method. I'm referring to the third commented line in your example - is that referring to just slots, or to slots and overriding __setattr__? Because it reads like the former, which is slightly misleading. I just want to make sure before I put in an edit, because I don't want to skew your meaning. Commented Feb 3, 2023 at 5:57
  • 1
    @ChaddRobertson I've removed most of the comment since one can redefine __slots__ in a subclass to add attributes when __setattrs__ is not overridden to prevent modification. Commented Feb 3, 2023 at 7:27
3

The object remains the same, even if you are changing properties of the object. And no, there are only very few immutable objects in python - frozenset for instance. But classes are not immutable.

If you want immutable objects, you have to make them so. E.g. forbid assigning new values to properties are turning new objects in that case.

To achieve this, you can use the underscore convention: Prepend your fields with a "_" - this indicates to other developers that the value is private and should not be changed from the outside.

If you want a class with an unchangeable "name" field you could use this syntax:

class test(object):
 def __init__(name):
 self._name = name
 @property
 def name(self):
 return self._name

Of course, _name CAN be changed by an dev, but that breaks the visible contract.

answered Feb 25, 2017 at 7:04
2

That is not the contract Python goes by From the docs- emphasis added by me on the bolded parts:

object.__hash__(self) Called by built-in function hash() and for operations on members of hashed collections including set, frozenset, and dict. __hash__() should return an integer. The only required property is that objects which compare equal have the same hash value; it is advised to mix together the hash values of the components of the object that also play a part in comparison of objects by packing them into a tuple and hashing the tuple. Example:

def __hash__(self):
 return hash((self.name, self.nick, self.color)) Note hash() truncates

And some more relevant information:

If a class does not define an __eq__() method it should not define a __hash__() operation either; if it defines __eq__() but not __hash__(), its instances will not be usable as items in hashable collections. If a class defines mutable objects and implements an __eq__() method, it should not implement __hash__(), since the implementation of hashable collections requires that a key’s hash value is immutable (if the object’s hash value changes, it will be in the wrong hash bucket).

And, to the core of your question:

User-defined classes have __eq__() and __hash__() methods by default; with them, all objects compare unequal (except with themselves) and x.__hash__() returns an appropriate value such that x == y implies both that x is y and hash(x) == hash(y).

A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None. When the __hash__() method of a class is None, instances of the class will raise an appropriate TypeError when a program attempts to retrieve their hash value, and will also be correctly identified as unhashable when checking isinstance(obj, collections.Hashable).

answered Feb 25, 2017 at 7:13
2

The reason is that to make this object hashable, despite the fact that it IS mutable, Python's default __hash__() method calculate the hash value from it's reference ID.

This means that if you change it's content or copy the reference to another name, the hash value won't change, But if you copy it to another place or create another object with the same content, then it's value will be different.

You can change that behaviour by redefining the __hash__() method, but you need to ensure that the object is not mutable or you will break your « named collections » (dictionnaries, sets & their subclasses).

answered Jan 2, 2018 at 3:27
1

As Guido Rossum once said, "We're all consenting adults here". So because allowing the programmer to shoot themselves in the foot is of highest priority, the code below clearly appears to be the most pythonic solution:

class C:
 """Please don't mutate."""
 ...
answered Dec 14, 2023 at 12:17
0

I will fill in the knowledge gaps in Christian's answer. From Python's official website (https://docs.python.org/2/reference/datamodel.html):

The value of an immutable container object that contains a reference to a mutable object can change when the latter’s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.

When I look at an object A whose byte data never change, that is truly immutable. The byte data may contains pointer to other mutable objects, but that doesn't mean the object A is mutable.

In your case, the object resides at a memory location. Python's hash generation is opaque. But if you are looking at things using the same reference, most likely the hash won't change, even when the bytes stored are different.

In a strict sense, mutable objects aren't even hashable, so you shouldn't try to interpret the hash in the first place.

To your question, just use a collections.namedtuple instead.

answered Feb 25, 2017 at 7:17
0

Another method to make your class immutable, if you are using Python 3.7 or later, is to use a dataclass with the frozen=True option. Here is your Person class, rewritten with this approach.

from dataclasses import dataclass
@dataclass(frozen=True)
class Person():
 name: str
 age: int

You can instantiate this class just as you did in your example.

>>> jane = Person('Jane', 29)
>>> print(jane.__hash__())
-8034965590372139066

But when you try to update the age attribute, you'll get an exception, because the instance is immutable.

>>> jane.age = 33
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'age'
answered Aug 9, 2021 at 15:51

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.