Python classes with __hash__ support (depending on the instance)
Question: I have a mutable class in Python which I would like to be able to "freeze", at
that point its immutable, therefor can have a `__hash__` function.

My concern is, will having the `__hash__` function present will make Python
behave strangely because it may check for the existence of a hash function.

I realize I could use a subclass that has a hash function, copy the class to a
subtype. But I'm interested to know if having an _optional_ hash function is
supported by Python.

In the example below it works in basic cases (but may fail in others).

Note: _This assumes you don't touch`_var` or `_is_frozen` directly and only
use access methods._

Note: its probably more Pythonic not to use this method and instead have a
`FrozenMyVar` class, but Im curious if this can be considered to be
_supported_ in Python or not.

    
    
    class MyVar:
        __slots__ = ("_var", "_is_frozen")
    
        def __init__(self, var):
            self._var = var
            self._is_frozen = False
    
        def freeze(self):
            self._is_frozen = True
    
        def __hash__(self):
            if not self._is_frozen:
                raise TypeError("%r not hashable (freeze first)" % type(self))
            return hash(self._var)
    
        def __eq__(self, other):
            try:
                return self.val == other.val
            except:
                return NotImplemented
    
        @property
        def var(self):
            return self._var
    
        @var.setter
        def var(self, value):
            if self._is_frozen:
                raise AttributeError("%r is frozen" % type(self))
            self._var = value
    
    
    # ------------
    # Verify Usage
    
    v = MyVar(10)
    v.var = 9
    
    try:
        hash(v)
    except:
        print("Hash fails on un-frozen instance")
    
    v.freeze()
    
    try:
        v.var = 11
    except:
        print("Assignment fails on frozen instance")
    
    print("Hash is", hash(v))
    

* * *

Adding a note on the real-world use-case, We have some linear math module with
Vector/Matrix/Quaternion/Euler classes. In some cases we want to have for eg,
a "set of matrices" or a "dict with vector keys". Its always possible to
expand them into tuples but they take up more memory & loose their abilities
to behave a our own math types - so the ability to _freeze_ them is
attractive.


Answer: The original example didn't quite work "sensibly", because the class had
`__hash__` but not `__eq__`, and as
[https://docs.python.org/3/reference/datamodel.html#object.**hash**](https://docs.python.org/3/reference/datamodel.html#object.__hash__)
says "If a class does not define an **eq**() method it should not define a
**hash**() operation either". But the OP's edit fixed that side issue.

This done, if the class and its instances are indeed used with the discipline
outlined, behavior should comply with the specs: instances are "born
unhashable" but "become hashable" -- "irreversibly" given said discipline, and
only, of course, if their `self.val` is in turn hashable -- once their
`freeze` method is called.

Of course `collections.Hashable` will "mis-classify" unfrozen instances (as it
only checks for the presence of `__hash__`, not its actual working), but that
is hardly unique behavior:

    
    
    >>> import collections
    >>> isinstance((1, [2,3], 4), collections.Hashable)
    True
    >>> hash((1, [2,3], 4))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unhashable type: 'list'
    

That `tuple` does appear "hashable", like all tuples (since its type does
define `__hash__`) -- but if you in fact try `hash`ing it, you nevertheless
get a `TypeError`, as one of the items is a `list` (making the whole not
actually hashable!-). Not-yet-frozen instances of the OP's class would behave
similarly to such a tuple.

An alternative which does avoid this little glitch (yet doesn't require
potentially onerous copies of data) is to model the "freezing" as the instance
"changing type in-place", e.g...:

    
    
    class MyVar(object):
        _is_frozen = False
    
        def __init__(self, var):
            self._var = var
    
        def freeze(self):
            self.__class__ = FrozenMyVar
    
        def __eq__(self, other):
            try:
                return self.val == other.val
            except:
                return NotImplemented
    
        __hash__ = None
    
        @property
        def var(self):
            return self._var
    
        @var.setter
        def var(self, value):
            if self._is_frozen:
                raise AttributeError("%r is frozen" % type(self))
            self._var = value
    
    
    class FrozenMyVar(MyVar):
        _is_frozen = True
    
        def __hash__(self):
            return hash(self._var)
    

This behaves essentially like the original example (I've removed the "slots"
to avoid issues with `object layout differs` errors on `__class__` assignment)
but may be considered an improved object model since "changing type in-place"
models well such irreversible changes in behavior (and as a small side effect
`collections.Hashable` now behaves impeccably:-).

The concept of an object "changing type in-place" freaks some out because few
languages indeed would even tolerate it, and even in Python of course it's a
rare thing to have a practical use case for such an obscure feature of the
language. However, use cases do exist -- which is why `__class__` assignment
is indeed supported!-)

