Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python 3 user defined immutable class objects

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.

like image 917
thatisvivek Avatar asked Feb 25 '17 06:02

thatisvivek


People also ask

Are objects from user defined classes immutable Python?

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.

Are objects in user defined classes mutable?

All objects (with the exception of a few in the standard library, some that implement special access mechanisms using things like descriptors and decorators, or some implemented in C) are mutable. This includes instances of user defined classes, classes themselves, and even the type objects that define the classes.

Can we create a user defined immutable class?

Immutable class in java means that once an object is created, we cannot change its content. In Java, all the wrapper classes (like Integer, Boolean, Byte, Short) and String class is immutable. We can create our own immutable class as well.

How do you make a class immutable in Python?

All you have to do is sub-class from Freezer . After initialization of the Python, I “freeze” the object with my overriding of the __delattr__ and __setattr__ methods on the object. I set _frozen = True which indicates the instance is now “frozen”. objects become immutable.


4 Answers

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

class Person:
    """Immutable person class"""

    # Using __slots__ reduces memory usage.
    # If __slots__ doesn't include __dict__, new attributes cannot be added.
    # This is not always desirable, e.g. it you want to subclass Person.
    __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
like image 119
Roland Smith Avatar answered Oct 23 '22 08:10

Roland Smith


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.

like image 26
Christian Sauer Avatar answered Oct 23 '22 07:10

Christian Sauer


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

like image 2
Camion Avatar answered Oct 23 '22 06:10

Camion


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

like image 1
juanpa.arrivillaga Avatar answered Oct 23 '22 08:10

juanpa.arrivillaga