Vistas de los diccionarios de Python: keys, values e items
Las “Vistas” (“Views” en inglés) de los datos que están dentro de los diccionarios (Mapping) en Python son muy importantes, tanto que se usan muchísimo (aunque no nos estemos dando cuenta de que lo usamos) y, sin embargo, no utilizamos su gran potencial. Un óptimo flujo de nuestros datos para que el procesamiento sea rápido y un consumo mínimo de recursos requiere conocer estos secretos.
La principal cualidad de las Vistas es que si modificamos algo de su diccionario asociado (algún valor o clave), se reflejará en el interior de las vistas (ejemplos más adelante).
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/vistas_de_los_diccionarios
Para que se entienda a la primera, para los ejemplos usaré el siguiente diccionario que tiene como clave una fruta (voy a escribir la clave en MAYÚSCULA para diferenciarla más fácilmente del valor que escribiré en minúscula) y como valor el árbol al que pertenece la fruta:
mi_diccionario = {
"MANZANA": "Manzano",
"UVA": "Vid",
"PERA": "Peral",
}
De este diccionario podremos querer “Ver” (Obtener Vistas en Python 3):
- Todas las claves con keys(), que devuelve un objeto de tipo KeysView.
vista_claves = mi_diccionario.keys()
- Todos los valores con values(), que devuelve un objeto de tipo ValuesView.
vista_valores = mi_diccionario.values()
- Todas las claves asociadas con sus valores (en tuplas) con items(), que devuelve un objeto de tipo ItemsView.
vista_items = mi_diccionario.items()
Vistas en Python 2
También se pueden obtener las vistas en Python 2, pero no si se utilizan los métodos keys(), values() o items() pues devuelven listados (o iteritems() que devuelve un “iterador”), estos métodos en Python 2 NO devuelven Vistas.
Para devolver una Vista en Python 2 es parecido, pero es necesario usar los métodos:
- viewkeys(): devuelve un objeto de tipo KeysView.
vista_claves = mi_diccionario.viewkeys()
- viewvalues(): devuelve un objeto de tipo ValuesView.
vista_valores = mi_diccionario.viewvalues()
- viewitems(): devuelve un objeto de tipo ItemsView.
vista_items = mi_diccionario.viewitems()
Clases de las Vistas (Views)
Como hemos comentado las clases de las vistas son:
- Vista de las Claves (KeysView)
- Vista de los Valores (ValuesView)
- Vista de los Ítems (ItemsView)
Y estas tres heredan de la clase Vista del Mapeo (MappingView).
Aunque las posibilidades con las Vistas (Views) son un poco más completas como veremos (más abajo dibujo un diagrama completo sobre las vistas; no lo he puesto aquí pues tiene unas cuantas flechas que de primera impresión pueden sorprender y, sin embargo, es bastante sencillo, quiero que sientas por ti mismo el porqué de las cosas y que es sencillo de entender).
Son Medibles (Sized)
Es decir, podremos utilizar “len()” para cada una de las Vistas, lo que nos devolverá la cantidad de elementos de su interior.
Por ejemplo, voy a querer la cantidad de claves de la “vista_claves” que es de tipo KeysView (funciona igual las otras dos vistas de tipo ValuesView y ItemsView):
cantidad_de_claves = len(vista_claves)
print("Cantidad de claves: {}".format(cantidad_de_claves))
Muestra:
Cantidad de claves: 3
Son iterables (Iterable)
Las Vistas son Iterables pues podremos recorrer sus elementos en un bucle FOR.
Por ejemplo, podremos iterar por cada uno de los elementos de “vista_valores” (también utilizar cualquiera de las tres vistas):
for valor in vista_valores:
print('Valor de mi_diccionario: {}'.format(valor))
Nos imprime:
Valor de mi_diccionario: Manzano
Valor de mi_diccionario: Vid
Valor de mi_diccionario: Peral
Ojo y cuidado, las Vistas NO son Iteradores (iterator); es decir, no se puede usar next() para recorrer elemento a elemento de la “vista”.
Recordatorio sobre Iterable vs Iterador (Iterator): no hay confundir “Iterable” que implementa __iter__ con “Iterador” que implementa __next__, ni tampoco con “Iterador-Iterable” que implementa ambos __iter__ y __next__; está explicado con detalle en el artículo sobre como crear nuestra propia colección https://jarroba.com/nuestra-propia-coleccion-en-python).
Las vistas con claves son Conjuntos (Set)
Las “vistas” que tengan claves, es decir, las de tipo KeysView y ItemsView son Conjuntos (Set). Por lo que quiere decir que podremos realizar cualquier operación de conjuntos sobre ellas (y quiere decir que los diccionarios han ganado la habilidad directa de ser “casi” conjuntos).
Supongamos que tenemos un conjunto de claves variadas (que llamaré “conjunto_claves”), de las cuales algunas se repiten a las claves de nuestro diccionario, como:
conjunto_claves = {"MANZANA", "PERA", "TOMATE"}
Queremos saber qué claves se repiten tanto en la “vista_claves” como en “conjunto_claves” a la vez (A ojo puedes ver que se repiten “MANZANA” y “PERA”). Aplicando la operación de conjuntos de “intersección” (cuyo símbolo es “&”) nos da ese resultado que queremos al devolver las claves comunes de ambos:
conjunto_claves_comunes = vista_claves & conjunto_claves
print('Conjunto de claves en ambos: {}'.format(conjunto_claves_comunes))
Y comprobamos por la consola como correctamente nos devuelve:
Conjunto de claves en ambos: {'MANZANA', 'PERA'}
Funciona igual con la vista de Ítems. Podemos crear un conjunto de tuplas, y cada tupla tendrá la clave y el valor. Un ejemplo, querremos que nos devuelva los elementos que no coincidan en los dos (es decir, queremos los elementos únicos de cada conjunto, los no repetidos en conjunto_tuplas_clave_y_valor y en vista_items); para ello nos es útil el operador sobre conjuntos de “diferencia simétrica” (cuyo símbolo es “^”):
conjunto_tuplas_clave_y_valor = {("MANZANA", "Manzano"), ("TOMATE", "Tomatero")}
conjunto_items_comunes = vista_items ^ conjunto_tuplas_clave_y_valor
print('Conjunto de ítems NO en ambos: {}'.format(conjunto_items_comunes))
Comprobamos que devuelve la consola un conjunto que no incluye a la tupla («MANZANA», «Manzano»), pues es la que se repite en ambos:
Conjunto de ítems NO en ambos: {('PERA', 'Peral'), ('LIMÓN', 'Limonero'), ('TOMATE', 'Tomatero'), ('UVA', 'Vid')}
Aunque aquí solo he utilizado las operaciones de “intersección” (&) y de “diferencia simétrica” (^), realmente las Vistas KeysView y ItemsView admiten cualquier operación sobre los conjuntos (todas las operaciones de conjuntos se pueden encontrar en https://docs.python.org/3.7/library/stdtypes.html#set).
ValuesView NO es de tipo Conjunto (Set), pero sí de tipo Colección (Collection). No es tipo Conjunto (Set) por el comportamiento de Tabla Hash de los diccionarios (más información de tablas hash en https://jarroba.com/tablas-hash-o-tabla-de-dispersion/), pues las claves son las únicas apuntadas directamente (es lo mismo que le ocurre a los conjuntos simples de Python) y los valores no.
Aunque ValuesView no puede trabajar con conjuntos directamente, se puede convertir esta Vista a un conjunto y operar igualmente (este truco es no es tan óptimo, ya que se tienen que recorrer todos los valores para crear el conjunto; por lo que cuidado si trabajas con ingentes cantidades de datos, puede que tengas que replantear las estructuras de datos).
conjunto_valores = {"Tomatero", "Vid", "Manzano"}
# Convertimos la vista en conjunto
conjunto_de_vista_valores = set(vista_valores)
conjunto_valores_comunes = conjunto_de_vista_valores & conjunto_valores
print('Conjunto de valores comunes: {}'.format(conjunto_valores_comunes))
Y al ejecutarlo vemos por pantalla que funciona:
Conjunto de valores comunes: {'Vid', 'Manzano'}
Clases de las Vistas (Views) extendidas
He aquí el diagrama de clases con todo lo que hemos aprendido hasta ahora (tanto en este artículo como en el anterior de crear nuestra propia colección https://jarroba.com/nuestra-propia-coleccion-en-python/ ):
Las vistas están asociadas a su diccionario
Como dijimos al principio del artículo: la principal cualidad de las Vistas es que si modificamos algo de su diccionario asociado (algún valor o clave), se reflejará en el interior de las vistas.
Comprobémoslo, obtengamos una de las vistas de nuestro diccionario, por ejemplo la vista de claves (con keys() obtenemos el objeto KeysView que se guardará en la variable “vista_claves”). Recorreremos la vista, modificaremos el diccionario, y volveremos a recorrer la vista para observar el cambio de las claves dentro de la vista (fíjate que la vista la obtenemos una única vez al principio del código):
# Obtenemos una vista; por ejemplo, las claves
vista_claves = mi_diccionario.keys()
# Recorremos las claves de dentro de la vista
for clave in vista_claves:
print('Clave de mi_diccionario ANTES: {}'.format(clave))
# Añadimos una clave y un valor que no exista en el diccionario
mi_diccionario["LIMÓN"] = "Limonero"
# Volvemos a recorrer las claves dentro de la vista para comprobar
for clave in vista_claves:
print('Clave de mi_diccionario DESPUES: {}'.format(clave))
Podemos comprobar como la primera vez que recorremos las vista nos devuelve unas claves (en la siguiente salida de consola aparece con el texto “ANTES”); modificamos el diccionario, volvemos a recorrer la vista para comprobar cómo la vista también ha sido afectada:
Clave de mi_diccionario ANTES: MANZANA
Clave de mi_diccionario ANTES: UVA
Clave de mi_diccionario ANTES: PERA
Clave de mi_diccionario DESPUES: MANZANA
Clave de mi_diccionario DESPUES: UVA
Clave de mi_diccionario DESPUES: PERA
Clave de mi_diccionario DESPUES: LIMÓN
Ciclo de vida de los métodos de las Vistas (Views) de un Mapeo (Mapping)
Si queremos entender al completo el funcionamiento de las Vistas y sobre todo si queremos construir nuestras propias clases de Mapeo (Mapping), será necesario conocer el ciclo de vida de las Vistas (evidentemente será necesario conocer esto siempre que queramos modificar su comportamiento).
El ciclo de la vida para obtener las claves ya lo hemos visto, aunque aquí profundizo un poco más, pues podrás ver que devuelve cada uno de los objetos de las Vistas (KeysView, ValuesView e ItemsView):
Y dependiendo del uso que le demos a cada una de las Vistas llamará a una cosa u a otra. Los ciclos de vida explicados en la siguiente imagen son:
- IF…IN llama al método mágico a __contains__. Como vimos en el artículo de como hacer “Nuestra propia colección en Python” (https://jarroba.com/nuestra-propia-coleccion-en-python), con
- Al iterar (en un bucle FOR) por la vista de KeysView llamará a __iter__, pero al iterar por las vistas ValuesView e ItemsView también llamará a __iter__ que obtiene las claves y tiene que llamar por cada clave a __getitem__ para obtener su valor.
- Al obtener el String de una Vista con str(), la vista llamará a __repr__ que será la encargada de mostrar la representación de dicha vista.
Veamos un ejemplo con nuestro propio Mapeo (Mapping) para entender cada uno de estos ciclos de vida.
Trabajar con Vistas en nuestro propio Mapeo (Mapping)
Para este ejemplo voy a querer un Mapeo (Mapping) que dado un número en centenas como clave (100, 200, 300), me devuelva como valor su texto (“CIEN”, “DOSCIENTOS”, “TRESCIENTOS”).
Pasos que haré en el código (recomiendo ojear el código que está a continuación por cada punto leído):
- En el siguiente código heredaré de Mapping porque solo quiero leer de mi diccionario personalizado (de mi objeto de tipo Mapping).
- Utilizaré dos listados para guardar en uno las claves y en otros los valores; así por ejemplo, la posición 2 en el listado de claves (lista_interna_claves) será 300 y corresponderá con la posición 2 de su valor que será “TRESCIENTOS” en el listado de valores (lista_interna_valores).
- Implementaré __len__ con un valor fijo, pues no puedo modificar el tamaño de mi diccionario de manera dinámica, y si en un futuro quiero llamar a len() me devolverá su tamaño.
- __iter__ tiene que devolver un “Iterador” y en una clase de tipo Mapping tiene que devolver obligatoriamente las claves. Por lo que quiero devolver el listado “lista_interna_claves” pero listado el “Iterable” no “Iterador”, por lo que podemos utilizar el método iter() para devolver su “Iterador”.
from collections import abc
class MiColeccionConVistas(abc.Mapping):
def __init__(self, *args, **kwargs):
self.lista_interna_claves = [100, 200, 300]
self.lista_interna_valores = ["CIEN", "DOSCIENTOS", "TRESCIENTOS"]
def __len__(self) -> int:
return 3
def __iter__(self):
return iter(self.lista_interna_claves)
def __getitem__(self, numero: int) -> str:
try:
posicion = self.lista_interna_claves.index(numero)
return self.lista_interna_valores[posicion]
except ValueError:
raise KeyError
Unas pruebas básicas rápidas para nuestro anterior código serían:
mi_coleccion_con_vistas = MiColeccionConVistas()
print('300 se escribe "{}"'.format(mi_coleccion_con_vistas[300]))
print('mi_coleccion_con_vistas tiene {} elementos'.format(len(mi_coleccion_con_vistas)))
if 200 in mi_coleccion_con_vistas:
print('200 existe en mi_coleccion_con_vistas')
else:
print('200 NO existe en mi_coleccion_con_vistas')
for valor in mi_coleccion_con_vistas:
print('Elemento de mi_coleccion_con_vistas: {}'.format(valor))
Y veríamos por consola:
300 se escribe "TRESCIENTOS"
mi_coleccion_con_vistas tiene 3 elementos
200 existe en mi_coleccion_con_vistas
Elemento de mi_coleccion_con_vistas: 100
Elemento de mi_coleccion_con_vistas: 200
Elemento de mi_coleccion_con_vistas: 300
Si hemos cumplido con la forma correcta de crear un Mapping no deberíamos tener ningún problema en poder obtener las Vistas (keys() para obtener KeysView, values() para obtener ValuesView e items() para obtener ItemsView), pues ya están implementadas en Mapping:
vista_claves = mi_coleccion_con_vistas.keys()
print("vista_claves: {}".format(vista_claves))
for clave in vista_claves:
print('Clave de mi_coleccion_con_vistas: {}'.format(clave))
vista_valores = mi_coleccion_con_vistas.values()
print("vista_valores: {}".format(vista_valores))
for valor in vista_valores:
print('Valor de mi_coleccion_con_vistas: {}'.format(valor))
vista_items = mi_coleccion_con_vistas.items()
print("vista_items: {}".format(vista_items))
for item in vista_items:
print('Ítem de mi_coleccion_con_vistas: {}'.format(item))
Podemos ver que nos imprime para cada Vista:
vista_claves: KeysView(<__main__.MiColeccionConVistas object at 0x000001F8E406E9E8>)
Clave de mi_coleccion_con_vistas: 100
Clave de mi_coleccion_con_vistas: 200
Clave de mi_coleccion_con_vistas: 300
vista_valores: ValuesView(<__main__.MiColeccionConVistas object at 0x000001A8D106BBE8>)
Valor de mi_coleccion_con_vistas: CIEN
Valor de mi_coleccion_con_vistas: DOSCIENTOS
Valor de mi_coleccion_con_vistas: TRESCIENTOS
vista_items: ItemsView(<__main__.MiColeccionConVistas object at 0x000001F7D126B4E8>)
Ítem de mi_coleccion_con_vistas: (100, 'CIEN')
Ítem de mi_coleccion_con_vistas: (200, 'DOSCIENTOS')
Ítem de mi_coleccion_con_vistas: (300, 'TRESCIENTOS')
Realmente si quisiéramos implementar cada uno de los métodos que nos devuelve la vista tendríamos que implementar lo siguiente (hay que pasarle el Mapping, que sería “self”, a cada clase que construya cada una de las vistas para que obtenga la información que necesite, por ejemplo KeysView(self) ):
class MiColeccionConVistas(abc.Mapping):
# …
def keys(self):
return abc.KeysView(self)
def values(self):
return abc.ValuesView(self)
def items(self):
return abc.ItemsView(self)
Otro detalle que nos falta es la representación sobrescribiendo a __repr__; pues las Vistas lo usan para representarse con str(). Sino nos pasará lo que ha ocurrido antes, que nos devuelve el objeto con la posición de memoria:
vista_claves: KeysView(<__main__.MiColeccionConVistas object at 0x000001F8E406E9E8>)
Queremos que nos dé información completa. Así que querremos ver un diccionario, por lo que tendremos que convertir dos listas a un diccionario; sabemos que el método de Python dict() si le pasamos un “Iterable” de tuplas (CLAVE, VALOR) nos creará un diccionario como lo queremos representar, y estas tuplas nos lo proporciona items(), por lo que podemos añadir a nuestra clase Mapping __repr__ (más información de __repr__ en https://jarroba.com/repr-y-str-de-python/):
class MiColeccionConVistas(abc.Mapping):
# …
def __repr__(self):
dict_claves_y_valores = dict(self.items())
return '{self.__class__.__name__}({dict_claves_y_valores})'.format(self=self, dict_claves_y_valores=dict_claves_y_valores)
Cuando se implementa __repr__ es muy recomendable modificar __init__ para que le lleguen los argumentos de __repr__ (ya que nos servirá para depurar y si no nos daría error nuestra herramienta de depuración). Por ejemplo “__init__(self, *args, **kwargs)”. En este ejemplo como es una clase Mapping que no admite mutabilidad (no se pueden añadir nuevos elementos), podría pensarse que no hacer falta, pero es una buena costumbre modificar __init__ ya que casi siempre hace falta.
Por lo que nuestra clase entera quedará (no añado keys(), values() e items() pues no he aportado nada antes salvo lo que ya hacía por defecto):
class MiColeccionConVistas(abc.Mapping):
def __init__(self, *args, **kwargs):
self.lista_interna_claves = [100, 200, 300]
self.lista_interna_valores = ["CIEN", "DOSCIENTOS", "TRESCIENTOS"]
def __len__(self) -> int:
return 3
def __iter__(self):
return iter(self.lista_interna_claves)
def __getitem__(self, numero: int) -> str:
try:
posicion = self.lista_interna_claves.index(numero)
return self.lista_interna_valores[posicion]
except ValueError:
raise KeyError
def __repr__(self):
dict_claves_y_valores = dict(self.items())
return '{self.__class__.__name__}({dict_claves_y_valores})'.format(self=self, dict_claves_y_valores=dict_claves_y_valores)
Ahora si imprimimos cada una de las vistas vemos que quedan bastante mejor:
vista_claves = mi_coleccion_con_vistas.keys()
print("vista_claves: {}".format(vista_claves))
vista_valores = mi_coleccion_con_vistas.values()
print("vista_valores: {}".format(vista_valores))
vista_items = mi_coleccion_con_vistas.items()
print("vista_items: {}".format(vista_items))
Y nos muestra la representación como la queríamos:
vista_claves: KeysView(MiColeccionConVistas({100: 'CIEN', 200: 'DOSCIENTOS', 300: 'TRESCIENTOS'}))
vista_valores: ValuesView(MiColeccionConVistas({100: 'CIEN', 200: 'DOSCIENTOS', 300: 'TRESCIENTOS'}))
vista_items: ItemsView(MiColeccionConVistas({100: 'CIEN', 200: 'DOSCIENTOS', 300: 'TRESCIENTOS'}))
Convertir Vistas en listados
Es bastante sencillo convertir una vista en un listado con list().
vista_claves = mi_coleccion_con_vistas.keys()
lista_claves = list(vista_claves)
Recuerda que la creación de un listado ocupará la memoria que necesite para llenase, si es la última opción mejor.
Bibliografía
- https://docs.python.org/3/library/stdtypes.html
- https://docs.python.org/3/library/collections.abc.html
- https://docs.python.org/3/library/abc.html
- https://github.com/python/cpython/blob/master/Lib/_collections_abc.py
- https://github.com/python/cpython/blob/master/Lib/abc.py
- https://twitter.com/1st1/status/1064572563509121024
- https://docs.python.org/2/library/collections.html
- https://docs.python.org/2/library/stdtypes.html
Gracias, me sirvió muchísimo. Seguire consultandote