Source code for mixinforge.mixins_and_metaclasses.immutable_mixin
"""Mixin for immutable objects with optimized hashing and equality.
Provides the ImmutableMixin class that enables value-based identity,
cached hashing, and optimized equality comparisons for objects that never
change after initialization. Subclasses define their identity through a
customizable key rather than through Python's id() function.
"""
from __future__ import annotations
from functools import cached_property
from typing import Any, Self
from .guarded_init_metaclass import GuardedInitMeta
[docs]
class ImmutableMixin(metaclass=GuardedInitMeta):
"""Base mixin for objects that never change after initialization.
Provides value-based identity semantics with optimized hashing and
equality comparisons. Instead of using object identity (id), instances
are compared based on a customizable identity key that represents their
immutable state. This enables efficient use in sets and dictionaries
while supporting value equality semantics.
The mixin caches the hash value for O(1) lookups and uses hash-based
short-circuiting in equality checks to avoid expensive comparisons.
This is particularly beneficial for complex objects with many fields.
Note that this mixin does not enforce immutability; subclasses are
responsible for ensuring their instances truly never change after
initialization.
Subclasses must override get_identity_key() to return a hashable value
that uniquely defines the object's identity based on its immutable state.
"""
[docs]
def __init__(self, *args, **kwargs):
"""Initialize the mixin.
"""
super().__init__(*args, **kwargs)
[docs]
def get_identity_key(self) -> Any:
"""Return a hashable value defining this object's identity.
Subclasses must override this method to specify what makes an
instance unique. The returned value is used for hashing and equality
comparisons, enabling value-based semantics. Common implementations
return a tuple of the object's immutable fields.
The returned value must be hashable and must remain constant for
the object's lifetime to maintain hash consistency.
Returns:
A hashable value uniquely identifying this object based on its
immutable state.
Raises:
NotImplementedError: If not overridden by subclass.
"""
raise NotImplementedError(
f"{type(self).__name__} must implement identity_key() method"
)
@cached_property
def identity_key(self) -> Any:
"""Cached identity key for consistent hashing and equality checks.
Caches the result of get_identity_key() to ensure the same value
is used throughout the object's lifetime. This guarantees hash
stability and enables efficient repeated comparisons without
recomputing the identity key.
Returns:
The cached identity key.
Raises:
RuntimeError: If called before initialization completes.
"""
if not self._init_finished:
raise RuntimeError("Cannot get identity key of uninitialized object")
return self.get_identity_key()
def __hash__(self) -> int:
"""Return hash based on the cached identity key.
Uses the cached identity key to compute the hash, ensuring O(1)
performance for repeated hash operations. This enables efficient
use in sets and dictionaries.
Returns:
Hash value derived from the identity key.
Raises:
RuntimeError: If initialization is incomplete.
"""
return hash(self.identity_key)
def __eq__(self, other: Any) -> bool:
"""Check equality based on type and identity key.
Implements optimized equality checking with multiple short-circuit
paths: identity check, type check, hash comparison, and finally
identity key comparison. The hash comparison provides fast rejection
for unequal objects without comparing full identity keys.
Args:
other: Object to compare against.
Returns:
True if types and identity keys match, False otherwise, or
NotImplemented for incompatible types.
"""
if self is other:
return True
elif type(self) is not type(other):
return NotImplemented
elif hash(self) != hash(other):
return False
else:
return self.identity_key == other.identity_key
def __ne__(self, other: Any) -> bool:
"""Check inequality based on type and identity key.
Delegates to __eq__ and inverts the result, maintaining consistency
with equality semantics and properly handling NotImplemented.
Args:
other: Object to compare against.
Returns:
True if objects are not equal, False otherwise, or
NotImplemented for incompatible types.
"""
eq_result = self.__eq__(other)
if eq_result is NotImplemented:
return NotImplemented
return not eq_result
def __copy__(self) -> Self:
"""Return self since immutable objects need no copying.
Immutable objects can safely share references instead of creating
copies, improving memory efficiency and performance.
"""
return self
def __deepcopy__(self, memo: dict[int, Any]) -> Self:
"""Return self since immutable objects need no deep copying.
Immutable objects can safely share references instead of creating
deep copies, improving memory efficiency and performance.
"""
return self