Creating custom container-like types is a common practice in the real world. Sometimes developers overwrite some magic methods for their special needs. Here is a case to overwrite the __repr__
and __hash__
methods. Both methods will traverse the object recursively which may run into an infinite recursion.
A minimal snippet to demonstrate the use case. There is a self-reference in the container-like object.
from __future__ import annotations
import reprlib
from typing import Iterable, Iterator, Sequence, SupportsIndex, TypeVar, overload
T_co = TypeVar('T_co', covariant=True)
class FrozenList(Sequence[T_co]):
def __init__(self, data: Iterable[T_co] = (), freeze: bool = False) -> None:
self.data: list[T_co] = list(data)
self._freeze = bool(freeze)
def freeze(self) -> None:
self._freeze = True
def clear(self) -> None:
if self._freeze:
raise ValueError('The list has been set frozen.')
self.data.clear()
def append(self, value: T_co) -> None:
if self._freeze:
raise ValueError('The list has been set frozen.')
self.data.append(value)
def extend(self, values: Iterable[T_co]) -> None:
if self._freeze:
raise ValueError('The list has been set frozen.')
self.data.extend(values)
@overload
def __getitem__(self, item: SupportsIndex) -> T_co:
...
@overload
def __getitem__(self, item: slice) -> FrozenList[T_co]:
...
def __getitem__(self, item):
if isinstance(item, slice):
return FrozenList(self.data[item], freeze=self._freeze)
return self.data[item]
@overload
def __setitem__(self, item: SupportsIndex, value: T_co) -> None:
...
@overload
def __setitem__(self, item: slice, value: Iterable[T_co]) -> None:
...
def __setitem__(self, item, value):
if self._freeze:
raise ValueError('The list has been set frozen.')
self.data[item] = value
def __len__(self) -> int:
return len(self.data)
def __iter__(self) -> Iterator[T_co]:
return iter(self.data)
def __eq__(self, other: object) -> bool:
return (
isinstance(other, FrozenList)
and self.data == other.data
and self._freeze == other._freeze
)
def __hash__(self) -> int:
if not self._freeze:
raise ValueError('The list has not been set frozen.')
return hash(tuple(self.data))
@reprlib.recursive_repr()
def __repr__(self) -> str:
return f'FrozenList({self.data!r}, freeze={self._freeze})'
if __name__ == '__main__':
fl = FrozenList()
fl.append(0)
fl.append(1)
print(fl) # -> FrozenList([0, 1], freeze=False)
fl.append(fl) # self-reference
print(fl) # -> FrozenList([0, 1, ...], freeze=False)
fl.freeze()
print(fl) # -> FrozenList([0, 1, ...], freeze=True)
print(hash(fl)) # -> RecursionError
The reprlib.recursive_repr()
decorator handles the recursion case for the __repr__
method well.
I wonder if it is worth adding a similar decorator for hash
. E.g.:
from _thread import get_ident
def recursive_hash(fillvalue=0):
'Decorator to make a hash function return fillvalue for a recursive call'
def decorating_function(user_function):
hash_running = set()
def wrapper(self):
key = id(self), get_ident()
if key in hash_running:
return fillvalue
hash_running.add(key)
try:
result = user_function(self)
finally:
hash_running.discard(key)
return result
# Can't use functools.wraps() here because of bootstrap issues
wrapper.__module__ = getattr(user_function, '__module__')
wrapper.__doc__ = getattr(user_function, '__doc__')
wrapper.__name__ = getattr(user_function, '__name__')
wrapper.__qualname__ = getattr(user_function, '__qualname__')
wrapper.__annotations__ = getattr(user_function, '__annotations__', {})
return wrapper
return decorating_function
Ref: