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.
7 Answers 7
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
-
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.ChaddRobertson– ChaddRobertson02/02/2023 13:31:20Commented Feb 2, 2023 at 13:31 -
@ChaddRobertson Have you tried it? You will get
AttributeError: Persons cannot be modified
.Roland Smith– Roland Smith02/02/2023 20:42:08Commented 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.ChaddRobertson– ChaddRobertson02/03/2023 05:57:11Commented 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.Roland Smith– Roland Smith02/03/2023 07:27:51Commented Feb 3, 2023 at 7:27
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.
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 functionhash()
and for operations on members of hashed collections includingset
,frozenset
, anddict. __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) andx.__hash__()
returns an appropriate value such thatx == y
implies both that x is y andhash(x) == hash(y)
.A class that overrides
__eq__()
and does not define__hash__()
will have its__hash__()
implicitly set toNone
. When the__hash__()
method of a class isNone
, instances of the class will raise an appropriateTypeError
when a program attempts to retrieve their hash value, and will also be correctly identified as unhashable when checkingisinstance(obj, collections.Hashable)
.
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).
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."""
...
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.
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'
__slots__ = ()
to disable instance__dict__
creation.