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.

thatisvivek
  • 875
  • 1
  • 12
  • 30
  • 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. – user2357112 Feb 25 '17 at 07:23
  • @user2357112 one can monkey-patch a class, so I am quite sure class instances are mutable. See http://stackoverflow.com/questions/5626193/what-is-a-monkey-patch – Patrick the Cat Feb 25 '17 at 07: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. – user2357112 Feb 25 '17 at 08:02

6 Answers6

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
Roland Smith
  • 42,427
  • 3
  • 64
  • 94
  • 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 Feb 02 '23 at 13:31
  • @ChaddRobertson Have you tried it? You will get `AttributeError: Persons cannot be modified`. – Roland Smith Feb 02 '23 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 Feb 03 '23 at 05: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 Feb 03 '23 at 07: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.

Christian Sauer
  • 10,351
  • 10
  • 53
  • 85
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).

Camion
  • 1,264
  • 9
  • 22
1

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).

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
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.

Patrick the Cat
  • 2,138
  • 1
  • 16
  • 33
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'
brocla
  • 123
  • 6