Mapeo (Mapping) y Mapeo Mutable (MutableMapping) en Python


Es muy interesante aprender a la vez Mapeo (Mapping) y Mapeo Mutable (MutableMapping) pues es raro que solo se necesite el Mapeo (Mapping) a secas, y aprender ambos a la vez es la manera más eficaz de aprender cómo funciona.

Antes de continuar, recomiendo leer rápidamente el anterior artículo sobre la especialización de los tipos de colección en Python en
https://jarroba.com/especializa-tu-propia-coleccion-en-python/ para tener una visión más amplia acerca de las diferentes estructuras de datos que nos puedan ayudar para crear nuestro nuevo tipo de datos.

Solo a modo de curiosidad y no es necesario para entender este artículo, si has usado por ejemplo bibliotecas como Pandas de Python (Pandas es una biblioteca especializada para analizar datos, sobre todo al hablar de “Big Data”; si tienes curiosidad sobre cómo utilizar Pandas, tienes más detalles en https://jarroba.com/pandas-python-ejemplos-parte-i-introduccion/), podrás advertir que por debajo han tenido que utilizar más de un truco que mostraremos en este artículo, por lo que con lo que en este artículo aprendas podrás crear tu propia biblioteca de tratamiento de datos a tu gusto 😉

Código en GitHub (Recomiendo primero leer el artículo y descargar/clonar el código al final):
https://github.com/Invarato/JarrobaPython/tree/master/mapeo_y_mapeo_mutable

Mapeo (Mapping)

Aunque probablemente ya lo sepas y puedas saltarte el siguiente breve cuadro, quiero aclarar que es mapear.

¿Qué es Mapear?

Supón que eres explorador y quieres identificar cada país con sus coordenadas en un mapa de papel (a esto se llama “cartografía” o también “mapear”). Entonces a cada nombre de cada país se asocia con sus coordenadas, por ejemplo (las coordenadas las he extraído de la Wikipedia, y las he redondeado):

  • España -> 40°Norte 4°Oeste
  • México -> 19°Norte 99°Oeste
  • Perú -> 12°Sur 77°Oeste
  • Argentina -> 34°Sur 58°Oeste

De este modo, si por ejemplo quiero saber ¿Dónde está “México”? En mi mapeo iría directamente a donde he escrito “México”, y sin tener que buscar más obtendría las coordenadas directamente (es decir, lo mismo que hace una “Tabla Hash” pues un diccionario lo es; más información en https://jarroba.com/tablas-hash-o-tabla-de-dispersion/ ).

El mapeo en los diferentes lenguajes de programación es semejante, llamándose “Diccionarios” (Por ejemplo, en Python “dict”) o “Mapas” (Por ejemplo, en Java se utiliza “Map”, como “HashMap”). Este artículo es de Python, por lo que si quisiéramos mapear lo anterior podemos utilizar un diccionario así:

mi_mapa = {
    'España': '40°Norte 4°Oeste',
    'México': '19°Norte 99°Oeste',
    'Perú': '12°Sur 77°Oeste',
    'Argentina': '34°Sur 58°Oeste'
}

Y en Python preguntaríamos a mi mapa (del diccionario que hemos llamado “mi_mapa”) por las coordenadas que tenga tal clave (por ejemplo, “Perú”) de este modo:

coordenadas_del_pais_elegido = mi_mapa['Perú']

La clase “Mapeo” (“Mapping”) es una especialidad de “Colección” (“Colection”), nos da la posibilidad de usar el objeto instanciado de nuestra propia clase como si fuera un diccionario de Python (dict). Es decir, si tenemos el diccionario:

mi_diccionario = {
    'clave1': 'valorA',
    'clave2': 'valorB', 
    'clave3': 'valorC'
}

Podemos obtener cualquier valor con su clave del siguiente modo:

valor = mi_diccionario['clave2']

También podremos recorrer sus claves insertadas en un bucle:

for clave in mi_diccionario:
    print('Clave de mi_diccionario: {}'.format(clave))

Si heredamos de Mapping nos pedirá sobrescribir los siguientes métodos “mágicos”:

from collections import abc


class MiMapeo(abc.Mapping):

    # Heredado de la clase Mapping
    def __getitem__(self, clave):
        pass

    # Heredado de la clase Sized (que pertenece a Collection)
    def __len__(self):
        pass

    # Heredado de la clase Iterable (que pertenece a Collection)
    def __iter__(self):
        pass

Como podrás deducir por el nombre del nuevo método mágico __getitem__ será el encargado de buscar el valor asociado a una clave y devolver dicho valor (dicho de otro modo, funcionará cuando escribamos con la sintaxis: mi_objeto[‘clave’] ).

Los métodos __len__ y __iter__ los puedes repasar en https://jarroba.com/nuestra-propia-coleccion-en-python/

¿Y dónde está el método mágico __contains__ de la clase “Contenedor” (“Container”) que pertenece “Collection” (Visto en el anterior artículo https://jarroba.com/nuestra-propia-coleccion-en-python/ )?

No es necesario de sobrescribir, ya que los “métodos mágicos” heredados que no son de obligada sobreescritura significa que ya están “escritos”. Es decir, que si los utilizamos sin sobrescribirlo van a funcionar (otra cosa más que nos facilita crear bien una Colección 😀 ), pero si queremos podremos sobrescribir su funcionamiento.

Aunque los métodos mágicos de sobreescritura opcional nos servirán la mayoría de las veces (por ejemplo, en el caso de __contains__ ya está programado por defecto que busque la clave y devuelva si existe o no); en ocasiones nos interesará sobrescribirlos para cambiar su funcionalidad (por ejemplo, si creamos un diccionario de palabras sinónimas en español, si una clave fuera “casa”, si preguntamos a __contains__ por la existencia de la clave “hogar” que es sinónimo de “casa”, nos interesa que devuelva “true”; y no “false” que sería lo que está programado por defecto al realizar una comparación directa, por lo que tendríamos que sobrescribir este método).

Ciclo de vida de los métodos de un Mapeo (Mapping)

Realmente no hay mucho ciclo de vida más que ampliar al que vimos en Colección (Collection) en el artículo https://jarroba.com/nuestra-propia-coleccion-en-python/. Añadimos El ciclo de vida de

  • Obtener un valor dada una clave ( valor = collection[«clave»] ) llama a __getitem__.
  • if «clave» in mapping: llama al método __contains__ para preguntar si tiene la clave. El __contains__ de Mapping por defecto tiene implementado que llame a __getitem__, pero no para obtener el valor, sino para ver si __getitem__ devuelve la excepción KeyError y si la devuelve significará que no tiene la clave por lo que entonces __contains__ devolverá «False», en caso de no recibir la excepción KeyError significará que la clave existe y devolverá «True».

Mapeo Mutable (MutableMapping)

Podríamos querer que nuestra implementación de un diccionario únicamente permita la consulta de sus ítems con __getitem__ (por ejemplo, si programamos alguna clase “Contract”; es decir, una clase que tenga valores finales que no queramos que se modifiquen y que solo se puedan leer).

Pero seguramente queramos que nuestra clase haga más ¿Qué pasa si queremos modificar o borrar los elementos que insertemos? Pues que tenemos que ofrecer todas las posibilidades mínimas que tiene cualquier estructura de datos que se precie (puede que te suenen las siglas “CRUD” muy utilizadas en las bases de datos y que vienen del inglés “Create, Read, Update, Delete”; y tienes información ampliada en https://jarroba.com/curso-de-bases-de-datos-con-mysql-parte-iii-video-definitionmanipulation/ y en https://es.wikipedia.org/wiki/CRUD ):

  • Inserción (o Alta o Crear): Insertar un nuevo elemento (clave y valor) en nuestro mapeo.
  • Consulta (o Leer): Consultar un elemento que exista en nuestro mapeo.
  • Modificación (o Actualizar): Modificar un elemento previamente insertado en nuestro mapeo.
  • Borrado (o Baja): Borrar un elemento que exista en nuestro mapeo.

La clase “Mapeo” (“Mapping”) nos ofrece las posibilidades inmutables; es decir, que NO se puede modificar el mapeo:

  • Consulta: con __getitem__. Permite utilizar la sintaxis siguiente para obtener el valor:
valor = mi_diccionario['clave']

Si queremos que se pueda modificar (que pueda “mutar”) utilizaremos la clase de “Mapeo Modificable” (“MutableMapping”), que hereda todo lo que vimos anteriormente de la clase “Mapeo” (“Mapping”).

Y “Mapeo Modificable” (“MutableMapping”) nos ofrece dos nuevos métodos mágicos para:

  • Inserción y Modificación: con __setitem__. Permite insertar un ítem (insertar la clave con su valor) nuevo o sustituirlo si ya existiera; escribiendo la siguiente sintaxis simplista de Python:
mi_diccionario['clave'] = 'valor a insertar o modificar'
  • Borrado: con __delitem__. Permite borrar fácilmente escribiendo:
del mi_diccionario['clave']

Heredando de “MutableMapping” se nos pedirá implementar justo lo que necesitamos para que nuestro mapeo modificable tenga sentido:

from collections import abc
from typing import Iterator


class MiMapeo(abc.MutableMapping):
    
    # Heredado de la clase Sized
    def __len__(self):
        pass
 
    # Heredado de la clase Iterable
    def __iter__(self) -> Iterator[_T_co]:
        pass
    
    # Heredado de la clase MutableMapping
    def __setitem__(self, clave, valor):
        pass
    
    # Heredado de la clase MutableMapping
    def __delitem__(self, valor):
        pass
    
    # Heredado de la clase Mapping
    def __getitem__(self, clave):
        pass

Uso básico de Mapeo Modificable (MutableMapping)

Queremos crear un diccionario para guardar únicamente dulces (como “galleta” o “gofre”) con sus calorías asociadas (es decir, la clave será el nombre del dulce y el valor va a ser un número que representará las calorías de ese dulce). Las cosas que no vamos a permitir guardar en nuestro diccionario las vamos a indicar en un conjunto de valores como el siguiente:

cosas_que_no_son_dulces = {
    "TELEVISION",
    "FLOR",
    "MARTILLO",
    "LAPIZ",
     "RELOJ"
}

Recordatorio sobre Conjuntos (set): inicializamos los valores de un Conjunto con la apertura de llaves {…} (en contraposición a los corchetes […] que indican que inicializamos un listado), y que un conjunto solo permite valores pero que no sean repetidos (utilizaremos conjuntos para que nuestro programa sea más rápido buscando valores, pues podríamos tener muchos valores; sino estás acostumbrado a utilizar conjuntos, simplemente funcionan igual que un listado, pero para las búsquedas un conjunto es mucho más rápido que un listado y además permite operaciones entre conjuntos).

Este conjunto indicará a nuestro diccionario que cosas NO están permitidas que guarde.

Y alimentaremos a nuestro diccionario personalizado con un diccionario simple con algunos valores como:

  • «Tortita»: 227
  • «Gofre»: 291
  • «Galleta»: 488
  • «Torrija»: 229
  • «Tarta»: 372
  • «Magdalena»: 377
  • «Croissant»: 466
  • «MARTILLO»: 1000000

El último es un “MARTILLO” que no debería admitir nuestro diccionario personalizado. Sin embargo, un diccionario simple admite cualquier valor como:

diccionario_simple_de_dulces_con_sus_calorias = {
    "Tortita": 227,
    "Gofre": 291,
    "Galleta": 488,
    "Torrija": 229,
    "Tarta": 372,
    "Magdalena": 377,
    "Croissant": 466,
    "MARTILLO": 1000000
}

Si llamamos a la clave NO permitida vemos que está dentro del diccionario simple:

print("Permite una clave no deseada -> {}: {}".format("MARTILLO", diccionario_simple_de_dulces_con_sus_calorias["MARTILLO"]))

Pues nos muestra:

Permite una clave no deseada -> MARTILLO: 1000000

Y no es lo que queremos. Lo que queremos es que, al añadir claves y valores, nuestro diccionario personalizado sea lo suficientemente listo como para descartar lo no deseado.

Ahora estudiemos la estructura del diccionario personalizado que crearemos (además queremos que se pueda utilizar tan fácilmente como los diccionarios simples de Python), por lo que:

  1. Tenemos que poder instanciarlo
  2. Tenemos que poder añadir claves y valores (en este ejemplo, la clave será el nombre del dulce y el valor un número entero con las calorías de este dulce. Aprovecho para indicar que las calorías me las he inventado)

Es decir, queremos que nuestro diccionario personalizado se pueda construir así:

# Instanciación de nuestro diccionario personalizado
mi_diccionario_de_dulces = MiDiccionarioEspecialDeDulces()

# Añadir elementos a nuestro diccionario personalizado
mi_diccionario_de_dulces["Gofre"] = 291
mi_diccionario_de_dulces["Galleta"] = 488

Además, nos falta una cosa, queremos añadir el conjunto de cosas que no son dulces, por lo que justo después de instanciarlo tendremos que añadirlo de alguna manera, para que se entienda fácil optamos por añadirlo con un método llamado “nuevo_conjunto_de_cosas_que_no_son_dulces”:

# Instanciación de nuestro diccionario personalizado
mi_diccionario_de_dulces = MiDiccionarioEspecialDeDulces()

# Conjunto con las cosas que no son dulces
cosas_que_no_son_dulces = {
    "TELEVISION",
    "FLOR",
    "MARTILLO",
    "LAPIZ",
    "RELOJ"
}

# Añadimos el conjunto de cosas que no son dulces a nuestro diccionario personalizado
mi_diccionario_de_dulces.nuevo_conjunto_de_cosas_que_no_son_dulces(cosas_que_no_son_dulces)

# Añadir elementos a nuestro diccionario personalizado
mi_diccionario_de_dulces["Gofre"] = 291
mi_diccionario_de_dulces["Galleta"] = 488

Tal y como queríamos, ya tenemos el uso sencillo de nuestro diccionario personalizado, ahora toca implementarlo.

Como hemos hecho antes, creamos una clase que llamaremos “MiDiccionarioEspecialDeDulces” que va a heredar de “MutableMapping” e implementamos los siguientes métodos dentro de la clase. Te resumo con palabras antes de enseñarte el código lo que va a hacer por dentro cada método (muchos seguramente ya los conozcas y deduzcas lo que van a hacer):

  • __init__: Donde inicializaremos a vacío las variables que van a guardar tanto los datos de los dulces como el conjunto de las cosas que no son dulces (luego explicaré la manera correcta de crear una inicialización en un diccionario, pues esta es simple para no recargar el código antes de haber entendido lo anterior).
  • __len__: Devolverá la cantidad de dulces guardados.
  • __iter__: Devolverá un iterador de los dulces (claves) ¿Y para iterar por los valores o los ítems? (Veremos unos ejemplos más adelante pero principalmente con las Views, que puedes ver en el anterior artículo https://jarroba.com/vistas-de-los-diccionarios-de-python-keys-values-e-items/)
  • __setitem__: Permitirá añadir un nombre de un dulce (clave) asociado con sus calorías (valor).
  • __delitem__: Permitirá borrar un dulce que exista (una clave que exista) de nuestro diccionario personalizado, sino existe devolverá error.
  • __getitem__: Permitirá obtener las calorías (valor) de un dulce (dada una clave).
  • nuevo_conjunto_de_cosas_que_no_son_dulces: este método que crearemos para dar nueva funcionalidad a nuestro diccionario personalizado guardará un nuevo conjunto de cosas que no son dulces; y luego comprobará que lo que estuviera insertado con anterioridad (antes de guardar este nuevo conjunto) cumpla con que no pertenezcan a este conjunto. Esta comprobación la hacemos aprovechando la velocidad que brinda la búsqueda de valores entre dos conjuntos (set) con la operación de intersección “&” (recuerdo que la intersección son los valores comunes de los dos conjuntos, por ejemplo: {A, B, C} & {D, B, C} = {B, C} ), y luego iteraremos borrando las pocas cosas que no son dulces (manera muy óptima, sobre todo si tenemos miles de datos en nuestro diccionario); la otra manera nada óptima de hacerlo sin utilizar las operaciones de conjuntos sería recorriendo todos los elementos que tenga nuestro diccionario preguntando por cada uno si es dulce o no (lo que es el bucle “for” de principio a fin de toda la vida; al pensar en diseñar tu propia colección recuerda siempre que puede llegar a guardar millones de elementos, tienes que intentar cumplir siempre con la manera más óptima posible).

Y ahora veamos cómo queda nuestro código:

Nota sobre la tipificación de parámetros: en los ejemplos voy a tipificar los parámetros de los métodos como se hace en Python 3; puedes optar por no tipificarlos como se hacía en versiones anteriores de Python, pero el IDE no te ayudará luego con los tipos y es bastante cómodo tanto para prevenir errores como para documentar en el propio código el tipo de datos que entra y devuelve el método.

from collections import abc
from typing import Iterator


class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    def __init__(self):
        self.diccionario_de_dulces_con_sus_calorias = {}
        self.cosas_que_no_son_dulces = set()

        # Nota: se inicializa un conjunto vacío con “set()” y no con “{}” porque sin inicializar con “valores” o “claves y valores” Python lo interpreta siempre como conjunto
    
    def __len__(self) -> int:
        cantidad_de_dulces = len(self.diccionario_de_dulces_con_sus_calorias)
        return cantidad_de_dulces
 
    def __iter__(self) -> Iterator[str]:
        iterador_de_dulces = self.diccionario_de_dulces_con_sus_calorias.__iter__()
        return iterador_de_dulces
    
    def __setitem__(self, clave: str, valor: int) -> None:
        if clave in self.cosas_que_no_son_dulces:
            print('Se ha descartado "{}" porque no es un dulce'.format(clave))
        else:
            self.diccionario_de_dulces_con_sus_calorias[clave] = valor
    
    def __delitem__(self, clave: str) -> None:
        try:
            del self.diccionario_de_dulces_con_sus_calorias[clave]
        except KeyError:
            raise KeyError("Este Contenedor no tiene la clave '{}'".format(clave))
    
    def __getitem__(self, clave: str) -> int:
        try:
            calorias_del_pastel = self.diccionario_de_dulces_con_sus_calorias[clave]
            return calorias_del_pastel
        except KeyError:
            raise KeyError("Este Contenedor no tiene la clave '{}'".format(clave))

    def nuevo_conjunto_de_cosas_que_no_son_dulces(self, cosas_que_no_son_dulces: set) -> None:
        # Si nos cuelan un None en el parámetro cosas_que_no_son_dulces lo traduciremos como conjunto vacío set() 
        if cosas_que_no_son_dulces is None:
            self.cosas_que_no_son_dulces = set()
        else:
            self.cosas_que_no_son_dulces = cosas_que_no_son_dulces

        # Al añadir un nuevo conjunto de cosas que nos son dulces nos interesa comprobar que no se hubiera colado nada que no se un dulce con anterioridad, por lo que lo comprobamos
        conjunto_de_claves_dulces = set(self.diccionario_de_dulces_con_sus_calorias.keys())
        conjunto_algo_no_dulce_entre_las_claves = conjunto_de_claves_dulces & self.cosas_que_no_son_dulces
        for clave_no_valida in conjunto_algo_no_dulce_entre_las_claves:
            print('Se ha eliminado "{}" porque no es un dulce'.format(clave_no_valida))
            del self.diccionario_de_dulces_con_sus_calorias[clave_no_valida]
        # Los valores que sean iguales en el conjunto_de_claves_dulces y (&) en el conjunto self.cosas_que_no_son_dulces serán las cosas que no son dulces que se han colado y hay que eliminarlo

A partir de aquí podemos probar como nuestro diccionario personalizado hace justo lo que queremos 🙂

Instanciamos nuestro diccionario y le añadimos el conjunto de cosas que NO son dulces:

mi_diccionario_de_dulces = MiDiccionarioEspecialDeDulces ()

cosas_que_no_son_dulces = {
    "TELEVISION",
    "FLOR",
    "MARTILLO",
    "LAPIZ",
    "RELOJ"
}

mi_diccionario_de_dulces.nuevo_conjunto_de_cosas_que_no_son_dulces(cosas_que_no_son_dulces)

Añadimos dulces y alguna cosa que no sea dulce para ver cómo se comporta:

mi_diccionario_de_dulces["Tortita"] = 227
mi_diccionario_de_dulces["Gofre"] = 291
mi_diccionario_de_dulces["Galleta"] = 488
mi_diccionario_de_dulces["Torrija"] = 229
mi_diccionario_de_dulces["Tarta"] = 372
mi_diccionario_de_dulces["Magdalena"] = 377
mi_diccionario_de_dulces["Croissant"] = 466
mi_diccionario_de_dulces["MARTILLO"] = 1000000

Si ejecutamos esto veremos por consola:

Se ha descartado "MARTILLO" porque no es un dulce

Como hemos intentado introducir un “MARTILLO” que pertenece al conjunto de “cosas_que_no_son_dulces” lo descarta (y nos avisa por consola, que para eso hemos puesto el “print()”; podríamos haber puesto un “raise KeyError” para que lanzara una excepción, pero no nos interesaba que diera error el programa, sino que descartara lo que no es válido y que continúe).

Como hemos implementado __iter__ podremos iterar por todas las claves insertadas:

for clave in mi_diccionario_de_dulces:
    print('Clave de mi_diccionario_de_dulces: {}'.format(clave))

Efectivamente nos mostrará todas las claves que son nuestros dulces:

Clave de mi_diccionario_de_dulces: Tortita
Clave de mi_diccionario_de_dulces: Gofre
Clave de mi_diccionario_de_dulces: Galleta
Clave de mi_diccionario_de_dulces: Torrija
Clave de mi_diccionario_de_dulces: Tarta
Clave de mi_diccionario_de_dulces: Magdalena
Clave de mi_diccionario_de_dulces: Croissant

Ahora vamos a eliminar algún dulce, como “Tarta”, y volver a iterar por todos los elementos para ver si efectivamente se ha eliminado:

del mi_diccionario_de_dulces["Tarta"]

for clave in mi_diccionario_de_dulces:
    print('Clave de mi_diccionario_de_dulces: {}'.format(clave))

Y como era de esperar ya no está:

Clave de mi_diccionario_de_dulces: Tortita
Clave de mi_diccionario_de_dulces: Gofre
Clave de mi_diccionario_de_dulces: Galleta
Clave de mi_diccionario_de_dulces: Torrija
Clave de mi_diccionario_de_dulces: Magdalena
Clave de mi_diccionario_de_dulces: Croissant

Probemos a preguntar si tiene algún dulce (clave), por ejemplo “Gofre” que sigue existiendo:

if "Gofre" in mi_diccionario_de_dulces:
    print('La clave "Gofre" EXISTE')
else:
    print('La clave "Gofre" NO EXISTE')

El dulce “Gofre” nos va a decir que existe ya que está dentro de nuestro diccionario:

La clave "Gofre" EXISTE

Si queremos obtener las calorías de este dulce:

valor = mi_diccionario_de_dulces["Gofre"]
print('Valor obtenido con la clave "Gofre" de mi_diccionario_de_dulces: {}'.format(valor))

Nos la tiene que dar:

Valor obtenido con la clave "Gofre" de mi_diccionario_de_dulces: 291

Pero y si no existe la clave, por ejemplo “Donut” no lo hemos añadido a nuestro diccionario, pues tendrá que funcionar también. Preguntamos si existe:

if "Donut" in mi_diccionario_de_dulces:
    print('La clave "Donut" EXISTE')
else:
    print('La clave "Donut" NO EXISTE')

Y efectivamente nos dice que no existe:

La clave "Donut" NO EXISTE

Como no existe dicha clave al intentar obtenerla nos tendría que devolver un error (englobamos en un “try/except” para capturar la excepción KeyError que demuestra que esa clave no existe):

try:
    valor = mi_diccionario_de_dulces["Donut"]
    print('Valor obtenido con la clave "Donut" de mi_diccionario_de_dulces: {}'.format(valor))
except KeyError as err:
    print('Error devuelto: {}'.format(err))

Y el error que nos ha devuelto:

Error devuelto: "Este Contenedor no tiene la clave 'Donut'"

Y si añadimos la clave “Donut” y luego la intentamos volver a obtener, ya existirá en nuestro diccionario personalizado:

mi_diccionario_de_dulces["Donut"] = 452

try:
    valor = mi_diccionario_de_dulces["Donut"]
    print('Valor obtenido con la clave "Donut" de mi_diccionario_de_dulces: {}'.format(valor))
except KeyError as err:
    print('Error devuelto: {}'.format(err))

Nos dirá que ya existe:

Valor obtenido con la clave "Donut" de mi_diccionario_de_dulces: 452

Uso completo de Mapeo Modificable (MutableMapping)

Hasta ahora hemos visto como crear un Mapeo mutable (MutableMapping) de una manera muy sencilla, pero falta completar ciertos detalles que son muy importantes, pero requeríamos conocer lo anterior para poder entender lo siguiente.

Partiendo del anterior ejemplo básico lo vamos a ir modificando para que cumpla con todas las propiedades para que sea un Contenedor correcto.

Paso de datos de inicio en el constructor

Previamente creamos el siguiente __init__ (constructor) sin parámetros:

class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    def __init__(self):
        self.diccionario_de_dulces_con_sus_calorias = {}
        self.cosas_que_no_son_dulces = set()

    # …

Es importante que puedan entrar todos los parámetros que se deseen en el constructor para reconstruir o inicializar nuestro diccionario de una forma más sencilla. En Python es una buena práctica poder volver a reconstruir la estructura de datos desde el __init__ (por ejemplo, desde la representación __repr__ como vimos en su artículo que le dedicamos https://jarroba.com/repr-y-str-de-python/).

Para esto nos podemos ayudar del método update() que incluye MutableMapping. update() pide un diccionario como parámetro e irá llamando a __setitem__ para cada pareja de clave y valor hasta que haya insertado todas. Para construir rápidamente el diccionario con dict() le pasamos tal cual todos los args (argumentos sin clave) y kwargs (argumentos con clave y valor “clave=valor”), y los expandiremos con los asteriscos (* y **) en dict() (Más información de los parámetros args y kwargs en https://jarroba.com/args-kwargs-python-ejemplos/). Añadiré un parámetro con clave especial que será “cosas_que_no_son_dulces” y va a pedir el conjunto (set) de cosas que no son dulces, que se pasará tal cual al método nuevo_conjunto_de_cosas_que_no_son_dulces() que para eso está (no se asigna directamente a “self.cosas_que_no_son_dulces”, pues recuerda que hace más comprobaciones el método). Con todo esto reconstruiremos completamente nuestro diccionario desde el __init__.

class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    def __init__(self, *args, cosas_que_no_son_dulces: set = None, **kwargs):
        self.diccionario_de_dulces_con_sus_calorias = {}
        self.cosas_que_no_son_dulces = set()

        self.nuevo_conjunto_de_cosas_que_no_son_dulces(cosas_que_no_son_dulces)

        self.update(dict(*args, **kwargs))

    # …

Ahora podremos reconstruir nuestro diccionario directamente desde el constructor (__init__). En el siguiente ejemplo voy a reconstruir el diccionario con cosas nuevas, le voy a pasar un diccionario con dos dulces nuevos y algo que no sea dulce, y a la vez le añadiré el conjunto de objetos que no sean dulces (con la clave especial que dijimos antes “cosas_que_no_son_dulces”). Y para probar que está bien guardado todo, iteraré por él:

mi_segundo_diccionario_de_dulces = MiDiccionarioEspecialDeDulces({'Caramelo': 147, 'Piruleta': 269, 'MESA': 10000000}, cosas_que_no_son_dulces={'SILLA', 'MESA'})

for clave in mi_segundo_diccionario_de_dulces:
        print('Clave de mi_diccionario_de_dulces: {}'.format(clave))

Vemos que funciona correctamente, que se ha eliminado lo que no es dulce que queríamos colar y que solo tiene las claves que son dulces:

Se ha descartado "MESA" porque no es un dulce

Clave de mi_diccionario_de_dulces: Caramelo
Clave de mi_diccionario_de_dulces: Piruleta

Otra manera de pasar los parámetros *args y **kwargs del constructor (sobre todo para Python 2)

Otra opción válida (y única en Python 2) que se suele ver bastante es que puedes no indicar el parámetro con la clave “cosas_que_no_son_dulces” (aunque el IDE no te ayuda) y luego obtenerlo en el diccionario que devuelve “kwargs” (y eliminarlo para que no se pase al método update() ) como en este ejemplo:

class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    def __init__(self, *args, **kwargs):
        self.diccionario_de_dulces_con_sus_calorias = {}
        self.cosas_que_no_son_dulces = set()

        cosas_que_no_son_dulces = kwargs["cosas_que_no_son_dulces"]
        self.nuevo_conjunto_de_cosas_que_no_son_dulces(cosas_que_no_son_dulces)
        del kwargs["cosas_que_no_son_dulces"]

        self.update(dict(*args, **kwargs))

    # …

Esto es cuestión de gustos, yo prefiero hacerlo como lo has visto previamente a este cuadro (que solo funciona desde Python 3). Mis principales motivos de escribir el nombre del parámetro son varios: el IDE ayuda a recordar el parámetro de la clase y su tipo, además que los parámetros extra se ven de un vistazo y ayuda en la documentación (y en la autogeneración de documentación).

De cualquier manera, te he mostrado las dos maneras de hacerlo.

Tienes más información en el artículo sobre los parámetros args y kwargs en https://jarroba.com/args-kwargs-python-ejemplos/

Representación de nuestro diccionario

Lo vimos en profundidad en el artículo de “repr y str” https://jarroba.com/repr-y-str-de-python/

Para representarlo para “máquinas” (desarrollo) simplemente __repr__ tiene que devolver algo una representación de nuestro diccionario que coincida con lo que pedíamos anteriormente en el constructor (__init__).

class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    # …

    def __repr__(self):
        return '{self.__class__.__name__}({self.diccionario_de_dulces_con_sus_calorias}, cosas_que_no_son_dulces={self.cosas_que_no_son_dulces})'.format(
            self=self)

Si lo probamos con nuestro primer diccionario:

print(repr(mi_diccionario_de_dulces))

Vemos que nos devuelve nuestra representación tal y como la habíamos diseñado:

MiDiccionarioEspecialDeDulces({'Tortita': 227, 'Galleta': 488, 'Torrija': 229, 'Tarta': 372, 'Magdalena': 377, 'Croissant': 466, 'Donut': 452}, cosas_que_no_son_dulces={'FLOR', 'MARTILLO', 'RELOJ', 'LAPIZ', 'TELEVISION'})

Esta representación la podríamos ejecutar y obtendríamos nuestro diccionario completo listo para continuar usándolo tal y como lo dejamos.

Y parar representarlo para humanos (para que sea vea bonito), sobrescribimos __str__. Por ejemplo, escribiré el texto “* <clave> tiene <valor> calorías”, y cada tupla de clave y valor irá en una línea diferente (el conjunto de cosas_que_no_son_dulces también lo podríamos escribir en __str__, yo no lo haré por no complicar más el código):

class MiDiccionarioEspecialDeDulces(abc.MutableMapping):

    # …

    def __str__(self):
        str_con_el_resultado = ''
        for nombre_del_pastel, calorias_del_pastel in self.diccionario_de_dulces_con_sus_calorias.items():
            str_con_el_resultado += "\n  * {} tiene {} calorías.".format(nombre_del_pastel, calorias_del_pastel)

        return str_con_el_resultado

Es muy rápido de probar:

print(mi_diccionario_de_dulces)

Y vemos que nos escribe de una forma más sencilla de leer:

  * Tortita tiene 227 calorías.
  * Galleta tiene 488 calorías.
  * Torrija tiene 229 calorías.
  * Tarta tiene 372 calorías.
  * Magdalena tiene 377 calorías.
  * Croissant tiene 466 calorías.
  * Donut tiene 452 calorías.

Claves, Valores e Ítems

Como vimos en el artículo sobre Vistas de los diccionarios https://jarroba.com/vistas-de-los-diccionarios-de-python-keys-values-e-items/, recuerda que puedes utilizar los métodos keys(), values() e ítems() para ver una representación de los datos que necesitemos. Simplemente podemos probar las tres Views (KeysView, ValuesView e ItemsView) sin tener que sobrescribir nada más (y así también vemos un uso de __repr__, pues las Views se pintan en base a lo que hayamos escrito en __repr__):

vista_claves = mi_diccionario_de_dulces.keys()
print("vista_claves: {}".format(vista_claves))
for clave in vista_claves:
    print('Clave de mi_diccionario_de_dulces: {}'.format(clave))

vista_valores = mi_diccionario_de_dulces.values()
print("vista_valores: {}".format(vista_valores))
for valor in vista_valores:
    print('Valor de mi_diccionario_de_dulces: {}'.format(valor))

vista_items = mi_diccionario_de_dulces.items()
print("vista_items: {}".format(vista_items))
for item in vista_items:
    print('Item de mi_diccionario_de_dulces: {}'.format(item))

Comprobamos:

vista_claves: KeysView(MiDiccionarioEspecialDeDulces({'Tortita': 227, 'Galleta': 488, 'Torrija': 229, 'Tarta': 372, 'Magdalena': 377, 'Croissant': 466, 'Donut': 452}, cosas_que_no_son_dulces={'RELOJ', 'FLOR', 'MARTILLO', 'LAPIZ', 'TELEVISION'}))
Clave de mi_diccionario_de_dulces: Croissant
Clave de mi_diccionario_de_dulces: Donut
Clave de mi_diccionario_de_dulces: Galleta
Clave de mi_diccionario_de_dulces: Magdalena
Clave de mi_diccionario_de_dulces: Tarta
Clave de mi_diccionario_de_dulces: Torrija
Clave de mi_diccionario_de_dulces: Tortita

vista_valores: ValuesView(MiDiccionarioEspecialDeDulces({'Tortita': 227, 'Galleta': 488, 'Torrija': 229, 'Tarta': 372, 'Magdalena': 377, 'Croissant': 466, 'Donut': 452}, cosas_que_no_son_dulces={'RELOJ', 'FLOR', 'MARTILLO', 'LAPIZ', 'TELEVISION'}))
Valor de mi_diccionario_de_dulces: 466
Valor de mi_diccionario_de_dulces: 452
Valor de mi_diccionario_de_dulces: 488
Valor de mi_diccionario_de_dulces: 377
Valor de mi_diccionario_de_dulces: 372
Valor de mi_diccionario_de_dulces: 229
Valor de mi_diccionario_de_dulces: 227

vista_items: ItemsView(MiDiccionarioEspecialDeDulces({'Tortita': 227, 'Galleta': 488, 'Torrija': 229, 'Tarta': 372, 'Magdalena': 377, 'Croissant': 466, 'Donut': 452}, cosas_que_no_son_dulces={'RELOJ', 'FLOR', 'MARTILLO', 'LAPIZ', 'TELEVISION'}))
Item de mi_diccionario_de_dulces: ('Croissant', 466)
Item de mi_diccionario_de_dulces: ('Donut', 452)
Item de mi_diccionario_de_dulces: ('Galleta', 488)
Item de mi_diccionario_de_dulces: ('Magdalena', 377)
Item de mi_diccionario_de_dulces: ('Tarta', 372)
Item de mi_diccionario_de_dulces: ('Torrija', 229)
Item de mi_diccionario_de_dulces: ('Tortita', 227)

Convertir en «iterador iterable»

Para iterar por las claves y valores de nuestro diccionario antes hemos utilizado el método __iter__ que devuelve un iterable, pero ¿Y si queremos hacer algo con cada valor a medida que se recorre el iterador? Es decir, aprovechar la iteración (en un bucle FOR) para realizar algún calculo (por ejemplo, podríamos contar las letras de cada clave mientras iteramos, ahorrándonos el tener que recorrer más veces todas las claves, lo que puede suponer un ahorro muy grande de recursos y además de tener los resultados en tiempo real/Streaming) o modificar el orden de las claves (por ejemplo, devolver en orden alfabético las claves).

Recordatorio sobre iterable vs iterador: no hay confundir “iterable” que implementa __iter__ con “iterador” que implementa __next__, ni tampoco con “iterador iterable” que implementa ambos __iter__ y __next__; se explica en detalle en el artículo anterior https://jarroba.com/nuestra-propia-coleccion-en-python).

Por ejemplo, haremos que los dulces estén ordenados alfabéticamente en un listado y cada vez que se itere el iterable (__iter__) que usa iterador (cada vez que se llame a __next__) devuelva la siguiente posición de ese listado. Para ello haremos los siguientes cambios en el código:

  • Añadimos abc.Iterator a la herencia de la clase.
  • En __init__ añadiremos un listado para guardar nuestra lista ordenada (que llamaré “lista_ordenada_de_nombres_de_pasteles”).
  • En __next__ hacemos dos cosas. Primero, si la “lista_ordenada_de_nombres_de_pasteles” es None, entonces obtiene las claves de nuestro diccionario y las ordena. Segundo, en cada llamada a __next__ des-apilamos (obtenemos el valor de un variable a la vez que la eliminamos de la lista) el primer elemento de la lista y lo devolvemos, pues será el siguiente valor ordenado; hasta que no quedan elementos que nos devolverá la excepción IndexError y paramos la iteración con StopIteration.
  • En __iter__ cada vez que se llame pondrá a None la variable ““lista_ordenada_de_nombres_de_pasteles” lo que la obliga a reiniciarse en la siguiente llamada a __next__.
class MiDiccionarioEspecialDeDulces(abc.MutableMapping, abc.Iterator):

    def __init__(self, *args, cosas_que_no_son_dulces: set = None, **kwargs):
        # …
        self.lista_ordenada_de_nombres_de_pasteles = None

    def __next__(self) -> str:
        if self.lista_ordenada_de_nombres_de_pasteles is None:
            lista_nombres_pasteles_desordenado = self.diccionario_de_dulces_con_sus_calorias.keys()
            self.lista_ordenada_de_nombres_de_pasteles = sorted(lista_nombres_pasteles_desordenado)

        try:
            return self.lista_ordenada_de_nombres_de_pasteles.pop(0)
        except IndexError:
            raise StopIteration

    def __iter__(self) -> Iterator[str]:
        self.lista_ordenada_de_nombres_de_pasteles = None
        return self

Podemos probar nuestro “iterador iterable”:

for clave in mi_diccionario_de_dulces:
    print('Clave de mi_diccionario_de_dulces: {}'.format(clave))

Comprobamos que efectivamente nos devuelve valor a valor ordenado:

Clave de mi_diccionario_de_dulces: Croissant
Clave de mi_diccionario_de_dulces: Galleta
Clave de mi_diccionario_de_dulces: Gofre
Clave de mi_diccionario_de_dulces: Magdalena
Clave de mi_diccionario_de_dulces: Tarta
Clave de mi_diccionario_de_dulces: Torrija
Clave de mi_diccionario_de_dulces: Tortita

Nota sobre el código anterior: Se podría haber devuelto el listado “lista_ordenada_de_nombres_de_pasteles” en el __iter__ y nos hubiéramos ahorrado todos los pasos en el __next__, pero aquí quería mostrarte un ejemplo con un resultado visible y directo al implementar el cuerpo de __next__. Podríamos haber hecho otras cosas interesantes en el __next__ en vez de devolver los valores ordenados, como por ejemplo contar las letras de cada clave/palabra cada vez que se fuera a devolver, lo que sería un procesamiento de la información por Streaming, un proceso por cada petición de datos; pero la devolución de la cantidad de letras habría que hacerlo en otro método diferente de la clase, lo que aquí me complicaría el código del ejemplo (devolver algo que no sea la clave en el “return” del __next__ rompería con el funcionamiento por defecto de MutableMapping).

Código completo

Simplemente te dejo aquí el código completo de este apartado para que lo puedas repasar en perspectiva:

class MiDiccionarioEspecialDeDulces(abc.MutableMapping,
                                    abc.Iterator):

    def __init__(self, *args, cosas_que_no_son_dulces: set = None, **kwargs):

        self.diccionario_de_dulces_con_sus_calorias = {}
        self.cosas_que_no_son_dulces = set()

        self.nuevo_conjunto_de_cosas_que_no_son_dulces(cosas_que_no_son_dulces)

        self.update(dict(*args, **kwargs))

        self.lista_ordenada_de_nombres_de_pasteles = None

    def nuevo_conjunto_de_cosas_que_no_son_dulces(self, cosas_que_no_son_dulces: set = None) -> None:
        """
        Método que sustituye al conjunto anterior y comprueba que todos los elementos del listado cumplan con que sean dulces
        :param cosas_que_no_son_dulces: set de valores de tipo str con las cosas que no son dulces
        :return: None
        """
        if cosas_que_no_son_dulces is None:
            self.cosas_que_no_son_dulces = set()
        else:
            self.cosas_que_no_son_dulces = cosas_que_no_son_dulces

        # Comprobación que no se cuele nada que no sea dulce en el diccionario de dulces
        conjunto_de_claves_dulces = set(self.diccionario_de_dulces_con_sus_calorias.keys())
        conjunto_algo_no_dulce_entre_las_claves = conjunto_de_claves_dulces & self.cosas_que_no_son_dulces
        for clave_no_valida in conjunto_algo_no_dulce_entre_las_claves:
            print('Se ha eliminado "{}" porque no es un dulce'.format(clave_no_valida))
            del self.diccionario_de_dulces_con_sus_calorias[clave_no_valida]

    def __getitem__(self, clave: str) -> int:
        try:
            calorias_del_pastel = self.diccionario_de_dulces_con_sus_calorias[clave]
            return calorias_del_pastel
        except KeyError:
            raise KeyError("Este Contenedor no tiene la clave '{}'".format(clave))

    def __setitem__(self, clave: str, valor: int) -> None:
        if clave in self.cosas_que_no_son_dulces:
            print('Se ha descartado "{}" porque no es un dulce'.format(clave))
        else:
            self.diccionario_de_dulces_con_sus_calorias[clave] = valor

    def __delitem__(self, clave: str) -> None:
        try:
            del self.diccionario_de_dulces_con_sus_calorias[clave]
        except KeyError:
            raise KeyError("Este Contenedor no tiene la clave '{}'".format(clave))

    def __len__(self) -> int:
        cantidad_de_dulces = len(self.diccionario_de_dulces_con_sus_calorias)
        return cantidad_de_dulces

    def __next__(self) -> str:
        if self.lista_ordenada_de_nombres_de_pasteles is None:
            lista_nombres_pasteles_desordenado = self.diccionario_de_dulces_con_sus_calorias.keys()
            self.lista_ordenada_de_nombres_de_pasteles = sorted(lista_nombres_pasteles_desordenado)

        try:
            return self.lista_ordenada_de_nombres_de_pasteles.pop(0)
        except IndexError:
            raise StopIteration

    def __iter__(self) -> Iterator[str]:
        self.lista_ordenada_de_nombres_de_pasteles = None
        return self

    def __repr__(self):
        return '{self.__class__.__name__}({self.diccionario_de_dulces_con_sus_calorias}, cosas_que_no_son_dulces={self.cosas_que_no_son_dulces})'.format(
            self=self)

    def __str__(self):
        str_con_el_resultado = ''
        for nombre_del_pastel, calorias_del_pastel in self.diccionario_de_dulces_con_sus_calorias.items():
            str_con_el_resultado += "\n  * {} tiene {} calorías.".format(nombre_del_pastel, calorias_del_pastel)
        return str_con_el_resultado

Ciclo de vida de los métodos de un Mapeo Mutable (MutableMapping)

Realmente no hay mucho ciclo de vida, pese a que en este artículo hemos aprendido un montón de cosas. El ciclo de vida de MutableMapping es:

  • Guardar (si existe se reemplaza) un valor en una clave ( mutable_mapping[“clave”] = valor ) llama a __setitem__.
  • Si queremos utilizar un diccionario para rellenar nuestro diccionario personalizado podemos utilizar update() que llama a __setitem__ por cada pareja de clave y valor en el diccionario que le pasemos.
  • Eliminar una clave con su valor ( del mutable_mapping[“clave”] ) llama a __del__.

Recordamos que MutableMapping hereda de Mapping, por lo que realmente tenemos todos estos ciclos de vida (más los de las Views que puedes encontrar en https://jarroba.com/vistas-de-los-diccionarios-de-python-keys-values-e-items/):

En perspectiva

Aquí dejo el diagrama con todas las clases que hemos visto hasta ahora en todos los artículos:

Bibliografía

Comparte esta entrada en:
Safe Creative #1401310112503
Mapeo (Mapping) y Mapeo Mutable (MutableMapping) en Python por "www.jarroba.com" esta bajo una licencia Creative Commons
Reconocimiento-NoComercial-CompartirIgual 3.0 Unported License.
Creado a partir de la obra en www.jarroba.com

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies

ACEPTAR
Aviso de cookies