from collections import deque
from itertools import chain
from sys import getsizeof
from typing import Any, Iterator, Optional


def total_size(o: Any, handlers: Optional[Any] = None) -> int:
    """Returns the approximate memory footprint an object and all of its contents.
    Automatically finds the contents of the following builtin containers and
    their subclasses:  tuple, list, deque, dict, set and frozenset.
    To search other containers, add handlers to iterate over their contents:
        handlers = {SomeContainerClass: iter,
                    OtherContainerClass: OtherContainerClass.get_elements}

    Based on https://github.com/ActiveState/recipe-577504-compute-mem-footprint/blob/master/recipe.py
    """
    handlers = handlers or {}

    def dict_handler(d: dict) -> Iterator[Any]:
        return chain.from_iterable(d.items())

    all_handlers = {
        tuple: iter,
        list: iter,
        deque: iter,
        dict: dict_handler,
        set: iter,
        frozenset: iter,
    }
    all_handlers.update(handlers)  # user handlers take precedence
    seen = set()  # track which object id's have already been seen
    default_size = getsizeof(0)  # estimate sizeof object without __sizeof__

    def sizeof(o: Any) -> int:
        if id(o) in seen:  # do not double count the same object
            return 0
        seen.add(id(o))
        s = getsizeof(o, default_size)

        for typ, handler in all_handlers.items():
            if isinstance(o, typ):
                s += sum(map(sizeof, handler(o)))  # type: ignore
                break
        return s

    return sizeof(o)
