Diccionarios y conjuntos#

Introducción
Diccionarios
Diccionarios_versus_listas
Operaciones con diccionarios
El diccionario como iterable
Ejemplos con Diccionarios
Conjuntos


Introducción#

La posibilidad de crear en memoria y manipular de una manera consistente colecciones de datos es una funcionalidad básica que brindan los lenguajes de programación. Python es particularmente eficaz en este cometido, porque ofrece muchos de estos tipos de datos compuestos de forma nativa.

Hemos utilizado con detalle las secuencias, especialmente las listas. Representan un conjunto secuencial (con un orden implícito) de datos, que pueden ser de diferentes tipos. La forma de acceder a los elementos es mediante el uso de un índice entero, que comienza en cero y que puede interpretarse como el desplazamiento a realizar desde el inicio de la secuencia hasta el elemento buscado. La lista es, desde luego, la colección más utilizada y es la adecuada para muchas aplicaciones.

En otros casos, cuando no existe un orden intrínseco en los datos, una organización diferente de éstos resulta más apropiada:

  • Los conjuntos son colecciones no ordenadas de valores no repetidos.

  • A mayores, los diccionarios son colecciones que permiten asociar los valores a una clave (key) para su cómodo acceso.

Nos centraremos principalmente en los diccionarios, que tienen un rango de aplicación muy superior al de los conjuntos.


Diccionarios#

Los diccionarios son colecciones iterables, no secuenciales y mutables de elementos compuestos por una clave (que identifica de modo único al elemento) y el valor que se desea almacenar. ¡No pueden aparecer elementos con la misma clave!

Su sintaxis es:

    {clave_1: dato_1, clave_2: dato_2, ..., clave_n: dato_n} 

Nótese el uso de llaves {..., ...} en lugar de corchetes [..., ...].

Veamos algunos ejemplos:

num = {1: 'uno', 2: 'dos', 3: 'tres', 4: 'cuatro'}

asig = {'asignatura': 'Fundamentos de Programación', 'créditos': 6, 'tipos': ['teoría', 'prácticas']} 

nom = {'persona': {'nombre': 'María', 'primer apellido': 'García', 'segundo apellido': 'Pérez'}, 'edad': 30}


print(num)
print(asig)
print(nom)
{1: 'uno', 2: 'dos', 3: 'tres', 4: 'cuatro'}
{'asignatura': 'Fundamentos de Programación', 'créditos': 6, 'tipos': ['teoría', 'prácticas']}
{'persona': {'nombre': 'María', 'primer apellido': 'García', 'segundo apellido': 'Pérez'}, 'edad': 30}

Como se observa, los diccionarios presentan una gran flexibilidad. Los valores pueden ser de cualquier tipo: datos simples, listas, incluso otros diccionarios, etc., con cualquier nivel de anidamiento. Las claves, sin embargo, tiene que ser tipos inmutables (enteros, cadenas, tuplas).

Para acceder individualmente a los elementos de los diccionarios, se utiliza la sintaxis de los corchetes.

num = {1: 'uno', 2: 'dos', 3: 'tres', 4: 'cuatro'}

print(num[1])
uno

En el caso de tener estructuras más complejas (anidadas) se deberá proceder en consecuencia. Por ejemplo:

nom = {'persona': {'nombre': 'María', 'primer apellido': 'García', 'segundo apellido': 'Pérez'}, 'edad': 30}

print(nom['persona']['primer apellido'])
García

En el ejemplo, en la clave persona del diccionario nom, se almacena a su vez otro diccionario. Para acceder a la clave primer apellido del diccionario anidado, se procede de la forma mostrada.

Otro ejemplo:

asig = {'asignatura': 'Fundamentos de Programación', 'créditos': 6, 'tipos': ['teoría', 'prácticas']} 

print(asig['tipos'][0])
teoría

En este caso, se accede al primer elemento de la lista que aparece como dato accesible a través de la clave tipos.


Diccionarios versus listas#

Tanto los diccionarios como las listas son colecciones iterables y mutables.

  • Iterables: los elementos que las componen pueden ser accedidos de uno en uno utilizando construcciones como el for.

  • Mutables: los elementos pueden ser modificados, las colecciones pueden ser borradas, extendidas, etc.

Las listas son colecciones secuenciales que establecen una relación de orden entre los elementos contenidos. Los diccionarios son no secuenciales: no tiene ningún sentido decir que un elemento cualquiera de un diccionario va primero que otro. El hecho de ser no secuenciales, por ejemplo, implica que en los diccionarios no se puedan utilizar los cortes (slices), como sí ocurre con las listas.

Todo lo que puede ser realizado con diccionarios puede ser llevado a cabo con listas. Sin embargo, para muchas tareas resulta más conveniente, por ser más intuitivo o más eficiente, el utilizar diccionarios.

Veamos un ejemplo resuelto de ambas formas. Se trata de programar un glosario reducido de términos de programación en Python.

conceptos = ['hashable', 'inmutable', 'interactivo', 'interpretado', 'iterable']
definiciones = ['Si su valor "hash" nunca cambia. Pueden actuar como "clave de diccionarios".',
                'Objeto con valor fijo.',
                'Python posee un intérprete interactivo.',
                'Python es un lenguaje interpretado.',
                'Objeto capaz de devolver sus elementos, uno cada vez.']

concepto = input('Diga concepto a buscar: ')

indice = conceptos.index(concepto)
definicion = definiciones[indice]

print(definicion)

Un ejemplo de ejecución sería el siguiente:

Diga concepto a buscar: interactivo
Python posee un intérprete interactivo.

En el código anterior se implementa el glosario utilizando dos listas: en la primera se almacenarían los conceptos a definir (siguiendo un orden lexicográfico, aunque en este caso el orden podría ser arbitrario) y en la segunda lista aparecen las definiciones correspondientes: aquí el orden tiene que ser consecuente con el orden elegido para la primera lista, cualquiera que este sea.

La tarea es realizable utilizando listas, pero resulta más compacta e intuitiva si se utilizan los diccionarios de Python.

# Glosario de Python implementado con un diccionario

glosario = {'hashable': 'Si su valor "hash" nunca cambia. Pueden actuar como "clave de diccionarios".',
            'inmutable': 'Objeto con valor fijo.',
            'interactivo': 'Python posee un intérprete interactivo.',
            'interpretado': 'Python es un lenguaje interpretado.',
            'iterable': 'Objeto capaz de devolver sus miembros, uno cada vez.'}

concepto = input('Diga concepto a buscar: ')
definicion = glosario[concepto]
print(definicion)

La diferencia entre las dos implementaciones previas (con listas y con diccionarios) sugiere que para esta aplicación particular, la solución basada en diccionarios es preferible.

  • Con los diccionarios se necesita mantener una única colección en memoria y no dos listas, que deberán estar necesariamente coordinadas, con lo que esto supone de una mayor susceptibilidad a errores.

  • A la hora de acceder a un valor (la definición) asociado a una clave (el concepto), simplemente se utiliza directamente la clave. En el caso de las dos listas, explícitamente hay que encontrar el índice que se corresponde con el concepto en la primera lista, para después acceder a la segunda utilizando dicho índice.

Se puede considerar al diccionario como una generalización de las listas. O a la inversa, una lista sería una especie de diccionario especializado que no tiene clave porque esa función sería realizada por el índice: los enteros en el intervalo [0, N-1] donde N es el número de elementos almacenados.

Debido a este hecho, la lista establece implícitamente una relación de orden entre los elementos. En el diccionario, al identificar individualmente a cada uno de los elementos por una clave explícita, dicho orden ya no tiene sentido.

Elementos que pueden actuar como claves en diccionarios#

Como se ha visto, las claves de los diccionarios pueden estar creadas usando diferentes tipos, no solo por cadenas de caracteres.

No todos los valores pueden ser claves. Solo lo pueden ser aquellos valores que sean inmutables:

  • caracteres

  • cadenas de caracteres

  • enteros, booleanos y reales

  • tuplas

No pueden actuar como claves ni las listas ni los propios diccionarios.


Operaciones con diccionarios#

Entre las operaciones a realizar con diccionarios se tienen:

  • Crear los diccionarios

  • Copiar diccionarios

  • Acceder a datos

  • Modificar datos

  • Extender

  • Borrar datos

  • Obtener tamaño

  • Recorrer diccionario

Los diccionarios (al igual que las listas) son estructuras de datos mutables. Esto quiere decir que se pueden modificar: cambiar el dato asociado a una clave, borrarlo, extender el diccionario con nuevos elementos, etc.

Véase el siguiente ejemplo, donde se parte de un diccionario vacío, para crearlo paso a paso.

Para actualizar el dato de un elemento en un diccionario basta con asignar un valor al elemento en cuestión, accediendo a él mediante la clave.

Si el elemento existe, se modifica. Si no existe, se crea uno nuevo: el diccionario se expande dinámicamente.

mediciones = {}
mediciones['temperatura'] = 30
mediciones['pres'] = 1.2
mediciones['nivel'] = 50
mediciones['valv'] = 'abierta'
print(mediciones)
{'temperatura': 30, 'pres': 1.2, 'nivel': 50, 'valv': 'abierta'}

La función dict()#

También es posible utilizar la función dict(), con diversos argumentos, para crear diccionarios de forma flexible.

  • Como conjunto variable de argumentos con nombre

    dic_arg    = dict(clave_1 = dato1, clave_2 = dato2, ...)     
  • Como una lista de tuplas

    dic_tuplas = dict([(clave_1, dato1), (clave_2, dato2)])     # 
  • Uniendo dos listas con la función zip()

    dic_listas = dict(zip([clave_1, clave_2], [dato1, dato2]))  # 
digitos = dict(uno=1, dos=2)
romanos = dict([(1, 'I'), (2, 'II'), (3, 'III'), (4, 'IV')])
por_dos = dict(zip([1, 2, 3], [2, 4, 6]))

print(digitos, romanos, por_dos)
{'uno': 1, 'dos': 2} {1: 'I', 2: 'II', 3: 'III', 4: 'IV'} {1: 2, 2: 4, 3: 6}

Copiar diccionarios#

Los diccionarios son colecciones mutables, al igual que las listas. Debido a ello, es importante ser consciente acerca de qué es lo que ocurre cuando se intenta copiar un diccionario a otro.

Alias#

Las asignaciones del tipo dic2 = dic1 simplemente crean un alias: otorgan otro nombre, dic2, a un diccionario también llamado dic1. Evidentemente, las modificaciones hechas a los datos subyacentes mediante un alias, serán accesibles cuando se acceda a través del otro.

Copia superficial (shallow copy)#

Hace una copia de los pares (clave: dato) de un diccionario a otro. No realiza copia de otras estructuras (listas, otros diccionarios) anidadas.

dic2_copia = dict(dic1)    # Utilizando la función dict()
dic3_copia = dic1.copy()   # Utilizando el método .copy() del diccionario fuente.
dic1 = {1: 2, 3: 4}
dic2 = dict(dic1)   
dic3 = dic1.copy()

dic1[1] = 100
print(dic1, 'id: ', id(dic1))
print(dic2, 'id: ', id(dic2))
print(dic3, 'id: ', id(dic3))
{1: 100, 3: 4} id:  2713687548992
{1: 2, 3: 4} id:  2713687547456
{1: 2, 3: 4} id:  2713687545920

Copia profunda (deep copy)#

Hace copia separadas de diccionarios con independencia del grado de anidamiento que presenten.

import copy

dir1 = {'Apt': 2, 'id': {'iq1': 1, 'iq2': 2}}
dir2 = copy.deepcopy(dir1)  # Copia profunda
dir3 = dir1.copy()  # Copia superficial
dir1['id']['iq1'] = 1000

print(dir1)
print('La copia profunda no sufre alteración con el cambio en dict1:', dir2)
print('La copia superficial se ve afectada con el cambio en dict1:', dir3)
{'Apt': 2, 'id': {'iq1': 1000, 'iq2': 2}}
La copia profunda no sufre alteración con el cambio en dict1: {'Apt': 2, 'id': {'iq1': 1, 'iq2': 2}}
La copia superficial se ve afectada con el cambio en dict1: {'Apt': 2, 'id': {'iq1': 1000, 'iq2': 2}}

Acceder a los elementos de forma segura#

Probablemente haya observado que, si se intenta acceder mediante la clave a un objeto que no se encuentra en el diccionario, se genera una excepción. Para evitar este tipo de errores se puede interrogar al diccionario por la existencia de la clave. Esto se puede realizar utilizando una estructura condicional y el operador in, que devuelve True si la clave indicada existe en el diccionario y False en caso contrario.

if 'temperatura' in mediciones:
    print(mediciones['temperatura'])
else:
    print('Probablemente deba ejecutar la celda previa')
30

Borrar elementos#

Igual que para las listas, se utiliza la función nativa del().

del mediciones['valv']

Pero también es posible utilizar el método .pop() del diccionario, para simultáneamente acceder al elemento señalado y borrarlo.

valor_temp = mediciones.pop('temperatura')
mediciones = {}
mediciones['temperatura'] = 30
mediciones['pres'] = 1.2
mediciones['nivel'] = 50
mediciones['valv'] = 'abierta'

valor = mediciones.pop('pres')
print(valor)
print(mediciones)
1.2
{'temperatura': 30, 'nivel': 50, 'valv': 'abierta'}

El método .clear() borra todos los elementos de un diccionario, dejándolo vacío.

Tamaño del diccionario#

La función len() está sobrecargada para trabajar también con diccionarios.

print(len(mediciones))

Uniendo diccionarios#

Una posibilidad interesante es la de unir dos diccionarios en uno. La sintaxis es:

dicionario1.update(dicionario2)

El método .update() del diccionario1 recibe otro diccionario, diccionario2 a modo de actualización.

El resultado es el siguiente:

  • Se añaden al diccionario actualizado todos los elementos del otro diccionario.

  • Si alguna clave coincide, el resultado final será el del elemento del segundo diccionario.

dic1 = {'a': 10, 'b': 1}
dic2 = {'a': 3, 'c': 0}

dic1.update(dic2)

print(dic1)
{'a': 3, 'b': 1, 'c': 0}

El diccionario como iterable#

El uso de los diccionarios en programación requiere frecuentemente de la posibilidad de recorrerlos, accediendo a cada uno de los elementos almacenados.

asig = {'asignatura': 'Fundamentos de Programación', 'créditos': 6, 'tipos': ['teoría', 'prácticas']} 

for clave in asig:
    print(asig[clave])
Fundamentos de Programación
6
['teoría', 'prácticas']

El anterior bucle es equivalente al siguiente:

for clave in asig.keys():
    print(asig[clave])

El método .keys() del diccionario devuelve un objeto iterable consistente en las claves del mismo, con lo cual su uso en este contexto es redundante.

Pero también es posible recorrer directamente los datos, obviando su relación con sus claves, utilizando el método .values(), como se ejemplifica a continuación.

for valor in asig.values():
    print(valor)
Fundamentos de Programación
6
['teoría', 'prácticas']

Existe todavía otra manera de recorrer el diccionario:

for clave, valor in asig.items():
    print(clave, '->', valor)
asignatura -> Fundamentos de Programación
créditos -> 6
tipos -> ['teoría', 'prácticas']

Utiliza el método .items() que devuelve una tupla conteniendo la clave y el dato asociado.


Ejemplos con Diccionarios#

Contar las veces que aparece cada letra (y otros signos de puntuación) en una cadena de caracteres.#

El algoritmo puede condensarse en los siguientes pasos:

  • Crear un diccionario vacío

  • Iterar para cada una de las letras de la cadena:

    • Si la letra no existe como clave en el diccionario:

      • Agregar la clave asignándole como valor al elemento un cero

    • Si ya existe

      • Incrementar en uno el entero asociado a la letra

texto = '''Este es un texto del que se van a extraer letras para analizar la frecuencia de las mismas.
La cadena tiene varias líneas, por eso utilizamos 3 comillas. También tiene dígitos y signos'''

letras_frec = {}

for letra in texto:
    if letra not in letras_frec:
        letras_frec[letra] = 1
    else:
        letras_frec[letra] += 1
    
print(letras_frec)
{'E': 1, 's': 15, 't': 9, 'e': 19, ' ': 31, 'u': 4, 'n': 10, 'x': 2, 'o': 7, 'd': 4, 'l': 9, 'q': 1, 'v': 2, 'a': 22, 'r': 8, 'p': 2, 'i': 12, 'z': 2, 'f': 1, 'c': 4, 'm': 5, '.': 2, '\n': 1, 'L': 1, 'í': 2, ',': 1, '3': 1, 'T': 1, 'b': 1, 'é': 1, 'g': 2, 'y': 1}

El código anterior es perfectamente válido y resulta muy claro.

En la medida en que conozcamos más en profundidad las diversas posibilidades que ofrece Python, podemos utilizarlas en nuestro beneficio. Por ejemplo, los diccionarios tienen el método .get(clave, valor) , que sirve el propósito de devolver:

  • si la clave existe, primer parámetro, se devuelve el valor asociado

  • si la clave no existe, se devuelve valor, el segundo parámetro, que es opcional

Pues bien, el uso de este método simplifica el código previo de la siguiente forma.

letras_frec = {}

for letra in texto:
    letras_frec[letra] = letras_frec.get(letra, 0) + 1
    
print(letras_frec)
{'E': 1, 's': 15, 't': 9, 'e': 19, ' ': 31, 'u': 4, 'n': 10, 'x': 2, 'o': 7, 'd': 4, 'l': 9, 'q': 1, 'v': 2, 'a': 22, 'r': 8, 'p': 2, 'i': 12, 'z': 2, 'f': 1, 'c': 4, 'm': 5, '.': 2, '\n': 1, 'L': 1, 'í': 2, ',': 1, '3': 1, 'T': 1, 'b': 1, 'é': 1, 'g': 2, 'y': 1}

Matrices dispersas (sparse)#

Existen muchas aplicaciones con matrices donde éstas se caracterizan por tener la gran mayoría de sus elementos con valor cero, y solo unos pocos con valores significativos. En estas aplicaciones, resulta más eficiente representar la matriz por un método alternativo, que consiste en representar solo los valores diferentes de cero, especificando la fila y la columna donde esos valores están situados. Las matrices susceptibles de ser representadas de esta forma son conocidas como matrices dispersas (sparse).

En el siguiente código se describe una función que recibe una matriz representada de la forma convencional (como una lista anidada de filas). Posteriormente, la convierte al formato de matriz dispersa utilizando un diccionario, en el que la clave está formada por una tupla que contiene la fila y columna del elemento a representar.

La matriz dispersa, ademas del diccionario, debe almacenar el tamaño de la matriz original. En el ejemplo, la matriz dispersa es una tupla formada por:

  • Una tupla con las dimensiones

  • El diccionario con las coordenadas y su valor diferente a 0

# Recibe matriz definida como lista anidada y devuelve matriz dispersa con diccionario
def matriz_to_dispersa(mat):
    mat_sparse = {}
    for r, fila in enumerate(mat):
        for c, elem in enumerate(fila):
            if elem != 0:
                mat_sparse[(r, c)] = elem
    return ((len(mat), len(mat[0])), mat_sparse)


# Recibe matriz dispersa y devuelve matriz como lista anidada
def dispersa_to_matriz(mat_dispersa):
    fil, col = mat_dispersa[0]
    dict = mat_dispersa[1]
    matriz = [[0]*col for _ in range(fil)]
    for (i, j), elem in dict.items():
        matriz[i][j] = elem
    return matriz


# Programa principal
mat = [[0, 0, 0, 1],
       [0, 1, 0, 0],
       [0, 0, 0, 0],
       [1.2, 0, 0, 0],
       [0, -3.1, 0, 0]]

mat_eficiente = matriz_to_dispersa(mat)
print(mat_eficiente)

mat = dispersa_to_matriz(mat_eficiente)
print(mat)
((5, 4), {(0, 3): 1, (1, 1): 1, (3, 0): 1.2, (4, 1): -3.1})
[[0, 0, 0, 1], [0, 1, 0, 0], [0, 0, 0, 0], [1.2, 0, 0, 0], [0, -3.1, 0, 0]]

Almacenando coordenadas de ciudades#

Supongamos que nos dan la siguiente información respecto a las capitales de provincia de la Comunidad Autónoma de Castilla y León, CyL.

Ciudad

UTM x

UTM y

Ávila

4501771

356599

Burgos

4687857

442361

León

4719468

289411

Palencia

4652764

373045

Salamanca

4538475

275839

Segovia

4533807

406015

Soria

4624007

544330

Valladolid

4612580

356065

Zamora

4598301

270015

La segunda y tercera columna representan las coordenadas (x, y) en metros del Sistema de Coordenadas Universal Transversal de Mercator (Universal Transverse Mercator, UTM). Todas estas ciudades pertenecen a la cuadrícula 30T. Las coordenadas (x, y) crecen respectivamente cuanto más al norte y al este están las ciudades.

Esta tabla puede almacenarse usando colecciones de múltiples formas. Veamos algunos ejemplos.

Mediante listas anidadas#

coord_ciudades = [['Ávila', (4501771, 356599)],
            ['Burgos', (4687857, 442361)],
            ['León', (4719468, 289411)],
            ['Palencia', (4652764, 373045)],
            ['Salamanca', (4538475, 275839)],
            ['Segovia', (4533807, 406015)],
            ['Soria', (4624007, 544330)],
            ['Valladolid', (4612580, 356065)],
            ['Zamora', (4598301, 270015)]]

Imaginemos que deseamos conocer las coordenadas UTM de Soria. Hay opciones más compactas, pero una posibilidad que refleja el coste computacional de esta tarea sería la siguiente:

ciudad = 'Soria'
coor_soria = None
for fila in coord_ciudades:
    if fila[0] == ciudad:
        coor_soria = fila[1]
        break

print(coor_soria)
(4624007, 544330)

Mediante dos listas#

Una forma menos compacta pero quizás más manejable sea separar en dos listas la información, manteniendo las posiciones correlativas inalteradas.

ciudades = ['Ávila', 'Burgos', 'León', 'Palencia', 'Salamanca',
            'Segovia', 'Soria', 'Valladolid', 'Zamora']
coord_ciudades = [(4501771, 356599), (4687857, 442361), (4719468, 289411),
                  (4652764, 373045), (4538475, 275839), (4533807, 406015),
                  (4624007, 544330), (4612580, 356065), (4598301, 270015)]

Volvamos a obtener las coordenadas de Soria.

ciudad = 'Soria'
coor_soria = None
for i, ciu in enumerate(ciudades):
    if ciu == ciudad:
        coor_soria = coord_ciudades[i]
        break

print(coor_soria)
(4624007, 544330)

Obviamente podríamos haber usado el método .index() de las listas, pero, en cualquier caso, detrás de las bambalinas tenemos un proceso de búsqueda secuencial:

ciudad = 'Soria'  # Prueba a poner Madrid
coor_soria = None
# La forma correcta de buscar el índice es con try, puesto que puede que el ítem buscado
# no esté en la lista y, en ese caso, se genera una excepción
try:
    indice = ciudades.index(ciudad)
except ValueError:
    print('La ciudad buscada no está en la lista.')
else:
    coor_soria = coord_ciudades[indice]
    print(coor_soria)
(4624007, 544330)

Dado que las listas son mutables y nuestros datos son inmutables, podríamos haber optado por tuplas como forma de almacenamiento.

En cualquier caso, vemos que el acceso a la información de las coordenadas de una ciudad es un procedimiento relativamente costoso. Tanto más cuanto mayor sea el número de ciudades. Es cierto que si mantenemos nuestra lista ordenada, en este caso alfabéticamente, podríamos optar en el caso de un listado muy grande a usar la búsqueda binaria para acelerar el proceso.

Este coste asociado a una búsqueda aparece en todo tipo de problemas. Pensad, por poner un ejemplo, en la búsqueda en un listín telefónico.

Almacenando los datos con diccionarios#

Utilizaremos la provincia como clave y las coordenadas como valor.

coord_ciudades = {'Ávila': (4501771, 356599),
            'Burgos': (4687857, 442361),
            'León': (4719468, 289411),
            'Palencia': (4652764, 373045),
            'Salamanca': (4538475, 275839),
            'Segovia': (4533807, 406015),
            'Soria': (4624007, 544330),
            'Valladolid': (4612580, 356065),
            'Zamora': (4598301, 270015)}

¡Volvamos a obtener las coordenadas de Soria!

try:
    coor_soria = coord_ciudades['Soria']
except KeyError:
    print('La ciudad buscada no está en la lista.')
else:
    print(coor_soria)
(4624007, 544330)

Sobran las palabras. El acceso a la información es inmediato. El código asociado es limpio y compacto.


Conjuntos#

Los conjuntos (sets) de Python son colecciones que tiene las siguientes características:

  • La colección es mutable, pero los elementos contenidos tienen que ser inmutables.

  • Los elementos no pueden aparecer repetidos.

  • Es iterable y no secuencial

A efectos prácticos, un conjunto es un diccionario donde las claves (elementos del conjunto) no tienen valores asociados.

Se pueden crear conjuntos especificando los elementos que lo conforman con la sintaxis:

{elem1, elem2, elem3, ...}

donde se utilizan las llaves {} al igual que con los diccionarios, aunque debe observar que los elementos se describen de forma diferente.

digitos = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}

Otra forma de crear conjuntos es utilizando la función set(), a la que se le pasa como argumento algún otro iterable.

Ejemplo:

lista_num = [1, 2, 3, 4, 3]
set_num = set(lista_num)
set_letras = set("Las letras de esta cadena formaran el conjunto, pero sin repeticiones.")

print(set_num)
print(set_letras)
{1, 2, 3, 4}
{'i', 'u', 'L', 'j', 'd', 'f', '.', ' ', 't', 'o', 'a', 'n', 'p', 's', ',', 'e', 'l', 'r', 'm', 'c'}

Es importante entender que los elementos del conjunto no pueden aparecer repetidos. Aunque tanto la lista como la cadena que sirve como fuente para crear el conjunto tienen elementos repetidos, el conjunto sólo almacena una instancia de cada elemento.

Observe además que la función print() está sobrecargada para poder sacar por pantalla objetos de tipo conjunto.

Modificando dinámicamente los conjuntos#

Al ser una colección mutable, se pueden añadir y borrar elementos del conjunto.

s = set()
s.add(1)
s.add('cad')
print(s)
{1, 'cad'}

En el fragmento de código se crea inicialmente un conjunto vacío. Observe que no se puede utilizar s = {}, porque resultaría ambiguo al confundirse con la sentencia que crea un diccionario vacío. Posteriormente, se utiliza el método .add() para añadir un elemento entero y a continuación otro de tipo str.

Para borrar elementos de un conjunto se tiene:

  • Método .clear(): borra todos los elementos.

  • Método .discard(elem): borra elemento elem si existe. Si no existe, no ocurre nada.

  • Método .remove(elem): elimina elemento elem si existe. Si no existe: se lanza excepción KeyError.

  • Método .pop(): elimina y devuelve elemento arbitrario. Si el conjunto está vacío: lanza excepción KeyError.

s = {1, 2, 3, 4, 5}

s.discard(9)
s.discard(4)
a = s.pop()

print('Elemento sacado con pop:', a, '\nConjunto que queda: ', s)
Elemento sacado con pop: 1 
Conjunto que queda:  {2, 3, 5}

En la medida en que el conjunto es una colección mutable, se tienen que tener en cuenta las implicaciones ya vistas en el caso de las listas y los diccionarios en lo relativo a la creación de alias y la copia superficial.

a = {1,2,3,4}
b = a

En lo anterior, b es simplemente un alias de a. Cualquier modificación de una variable o de la otra que ocurra después de la asignación, modifica el dato común que es accedido a través de cualquiera de ellas.

Si se quiere obtener otro conjunto que inicialmente contenga los mismos elementos que otro, se utilizará el método .copy() del conjunto fuente, como se indica en el ejemplo.

a = {1, 2, 3, 4}
b = a.copy()
b.add(10)
print(a, b)
{1, 2, 3, 4} {1, 2, 3, 4, 10}

El conjunto como colección iterable#

Un conjunto es una colección iterable. De manera que se puede utilizar en aquellas construcciones que espera este tipo de elementos, como por ejemplo bucles for.

vocales = set('aeiou')
cons_preferidas = set('pm')

for consonante in cons_preferidas:
    for vocal in vocales:
        print(consonante + vocal)
mi
mu
mo
ma
me
pi
pu
po
pa
pe

Métodos de la clase conjunto y funciones útiles#

Existen funciones nativas de Python que están sobrecargadas para trabajar con conjuntos de la misma forma que con cualquier otro iterable.

Por ejemplo:

  • sum(): Suma todos los elementos (en caso de que la suma este definida para esos elementos)

  • len(): Devuelve un entero con el número de elementos.

  • min(), max(): Devuelven el mínimo y el máximo respectivamente de los elementos en el conjunto.

  • sorted(): Devuelve una lista con los elementos del conjunto ordenados.

  • list(), tuple(), enumerate(): Devuelve una lista, una tupla o un enumerado respectivamente con los datos del conjunto que se le pasa como parámetro.

s = set(range(6))
print(list(enumerate(s)))
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]

Operaciones matemáticas entre conjuntos#

La característica distintiva y la utilidad mayor de los conjuntos se obtiene precisamente de su capacidad para representar el comportamiento de los conjuntos matemáticos.

Las operaciones más importantes en este sentido son:

  • Determinación de si un elemento o, en general, un subconjunto pertenece o no al conjunto.

  • Obtención del conjunto que resulta de la unión de dos conjuntos.

  • Obtención del conjunto que es la diferencia entre un conjunto y otro.

  • Obtención del conjunto que resulta de la intersección de dos conjuntos.

En lo que sigue, vemos con algo más detalle cada una de estas operaciones.

Pertenencia de un elemento o subconjunto a otro#

Las operaciones de esta subsección dan como resultado un valor lógico. Para el caso de la pertenencia o no de un elemento a un conjunto, se tendría que el resultado sería True si:

Operación

Representación matematica

Python

a es un elemento de A

\(a \in A\)

a in A

b no es un elemento de A

\(b \notin A\)

b not in A

A es igual a B

\(A = B\)

A == B

B es subconjunto de A

\(B \subseteq A\)

B.issubset(A)

A es un superconjunto de B

\(A \supseteq B\)

A.issuperset(B)

A y B son disjuntos

\(A \cap B = \emptyset\)

A.isdisjoint(B)

a = {1, 2, 3}
b = {3, 2}
print(a.issubset(b))
print(a.issuperset(b))
print(b.isdisjoint(a))
False
True
False

Unión, intersección y diferencia de conjuntos#

Sets.jpg

Estas operaciones sobre conjuntos dan como resultado otro conjunto.

  • El conjunto unión contiene todos los elementos de sus conjuntos operandos.

  • El conjunto intersección contiene los elementos que pertenecen simultáneamente a ambos conjuntos.

Tanto la unión como la intersección son operaciones simétricas. Ej: a.union(b) == b.union(a).

La diferencia, sin embargo, no es simétrica: el conjunto diferencia contiene todos los elementos del primer operando que no están en el segundo.

Una operación relacionada con la anterior es la que actualiza los elementos de A, eliminado aquellos que están en B.

Operación

Representación matematica

Python

A unión B

\(C = A \cup B\)

C = A.union(B)

A intersección B

\(C = A \cap B\)

C = A.intersection(B)

A diferencia de B

\(C = A - B\)

C = A.difference(B)

A actualiza con B

\(A = A - B\)

A.difference_update(B)

Ejemplo#

# Función que recibe lista y devuelve otra sin elementos repetidos usando set
def elimina_rep(lista):
    return list(set(lista))


# Programa principal
lista = [1, 2, 1, 4, 3, -2, 1, 3, 2, 2, 9]
print(elimina_rep(lista))       
[1, 2, 3, 4, 9, -2]

La función en el ejemplo anterior recibe una lista, que puede tener o no elementos repetidos, y devuelve otra que contiene los elementos de la lista original pero representados solamente una vez. La versión usando listas resulta menos compacta.

# Función que recibe lista y devuelve otra sin elementos repetidos usando listas
def elimina_rep(lista):
    lista_sin_repetidos = []
    for x in lista:
        if x not in lista_sin_repetidos:
            lista_sin_repetidos.append(x)
    return lista_sin_repetidos

# Programa principal
lista = [1, 2, 1, 4, 3, -2, 1, 3, 2, 2, 9]
print(elimina_rep(lista))    
[1, 2, 4, 3, -2, 9]

En este caso, se conserva el orden relativo de los elementos de la lista original.

Comentario sobre eficiencia de diccionarios y conjuntos#

Los diccionarios y los conjuntos de Python pueden ser muy útiles a la hora de ofrecer soluciones inesperadamente simples a problemas que de otra forma requerirían un mayor esfuerzo. Constituyen un recurso de alto nivel de abstracción que el lenguaje pone a disposición del programador.

Una mayor abstracción, al evitar tener que prestar atención a los detalles, permite una mayor productividad en la tarea de programación.

Pero no todo son ventajas. Una mayor abstracción muchas veces implica un mayor coste computacional al implicar la interposición de capas de software entre la descripción abstracta del problema general y la solución particular de cada caso.

La buena noticia es que los diccionarios y los conjuntos de Python tienen un coste computacional relativamente bajo. Son implementados como tablas de dispersión muy eficientes. El estudio de la estructura de las tablas de dispersión (Hash tables) queda fuera del ámbito de la asignatura.