"""Comportamiento real del dict de CPython. Se corre con python3 dict_demo.py."""
import sys


def show_order():
    d = {"lima": 0, "cusco": 1, "puno": 2}
    print("creado:         ", list(d))
    d["puno"] = 9
    print("reemplazar puno:", list(d))
    del d["cusco"]
    d["tacna"] = 3
    print("borrar cusco y agregar tacna:", list(d))
    d["cusco"] = 7
    print("volver a agregar cusco:      ", list(d))
    print("popitem saca el último:", d.popitem())


def expected_size(log2_size):
    """Lo que predice Objects/dictobject.c para claves de texto: 64 del objeto dict con el
    encabezado del recolector, 32 del encabezado de las claves, los índices y las entradas de
    16 bytes, 2/3 de las casillas."""
    size = 1 << log2_size
    index_bytes = 1 if size <= 128 else 2 if size <= 2**15 else 4
    return 64 + 32 + size * index_bytes + (size * 2 // 3) * 16


def show_sizes():
    """La predicción no mira a sys.getsizeof: sale de las reglas de dictobject.c. Se empieza con 8
    casillas y, cuando se acaban las entradas usables (2/3), el tamaño nuevo es la menor potencia
    de 2 que llegue a used * 3. Sin borrados, used es la cantidad de claves que ya hay."""
    print("claves  sys.getsizeof  casillas  usables  predicción")
    log2_size = 3
    d = {}
    last = sys.getsizeof(d)
    for i in range(400):
        used = len(d)
        if used == (1 << log2_size) * 2 // 3:
            log2_size = 3
            while (1 << log2_size) < used * 3:
                log2_size += 1
        d[f"clave{i}"] = i
        size = sys.getsizeof(d)
        if size != last:
            slots = 1 << log2_size
            print(f"{i + 1:6} {size:14} {slots:9} {slots * 2 // 3:8} {expected_size(log2_size):11}")
            last = size


def show_cleaning():
    """Con pocas claves vivas, la reconstrucción no crece: la tabla se achica."""
    d = {f"k{i}": i for i in range(1000)}
    before = sys.getsizeof(d)
    for i in range(990):
        del d[f"k{i}"]
    print(f"1000 claves: {before} bytes; al borrar 990: {sys.getsizeof(d)} bytes")
    i = 0
    while sys.getsizeof(d) == before:
        d[f"z{i}"] = i
        del d[f"z{i}"]
        i += 1
    print(f"tras {i} altas y bajas, con {len(d)} claves: {sys.getsizeof(d)} bytes")


def main():
    print("Python", sys.version.split()[0], sys.implementation.name)
    show_order()
    print()
    show_cleaning()
    print()
    show_sizes()


if __name__ == "__main__":
    main()
