6. Listas, tuplas, conjuntos y diccionarios¶
La gama de contenedores de Python hay que conocerla con cierta profundidad para hacer programas no triviales. Estudia los capítulos 8, 9 y 10 del libro de texto y empieza a practicar.
Directa o indirectamente hemos conocido varias formas de agrupar elementos en Python. La que hemos utilizado con más frecuencia es la lista, que se crea con corchetes. Alguna vez han aparecido las tuplas que aparecen entre paréntesis. Y aún hay otra forma, que son los conjuntos, que se expresan como una secuencia de elementos entre llaves.
a = [ 1, 2, 3 ]
b = ( 1, 2, 3 )
c = { 1, 2, 3 }
d = {}
type(d), type(a), type(b), type(c)
(dict, list, tuple, set)
Aparentemente listas y tuplas funcionan de forma parecida.
print(a[1:])
for i in a:
print(i)
if 3 in a:
print('El 3 está')
[2, 3]
1
2
3
El 3 está
print(b[1:])
for i in b:
print(i)
if 3 in b:
print('El 3 está')
(2, 3)
1
2
3
El 3 está
Los conjuntos son ligeramente diferentes, porque no permiten indexar, pero sí iterar y determinar pertenencia. De hecho son especialmente eficientes en esta última operación.
# c[1:] <- No podemos
for i in c:
print(i)
if 3 in c:
print('El 3 está')
1
2
3
El 3 está
Los diccionarios se comportan como conjuntos, pero además tienen una operación de indexación especial, que permiten acceder a valores independientes de los elementos que actúan como índice.
d = { 1:'uno', 2:'dos', 3:'tres' }
for i in d:
print(i, d[i])
if 3 in d:
print('El 3 está')
1 uno
2 dos
3 tres
El 3 está
Otra característica distintiva de conjuntos y diccionarios es que el orden no necesariamente refleja el orden en que se introdujeron los elementos. Se dice que son contenedores sin orden.
c = { 3, 1, 2 }
d = { 2:'dos', 1:'uno', 3:'tres' }
for i in c:
print(i)
for i in d:
print(i, d[i])
1
2
3
1 uno
2 dos
3 tres
6.1. Mutabilidad en listas¶
Las listas tienen algunas características especiales, que deben ser conocidas para evitar sorpresas. Por ejemplo, se trata de un tipo mutable. Esto quiere decir que podemos usar el operador de indexación (corchetes) al lado izquierdo de una asignación.
a = list(range(20))
a[19] = 58
a[5:12] = []
print(a)
[0, 1, 2, 3, 4, 12, 13, 14, 15, 16, 17, 18, 58]
La mutabilidad tiene consecuencias muy importantes. Vamos a ver una serie de ejemplos para ilustrarlas.
consonantes = ['b', 'c', 'd', 'f']
vocales = ['a', 'e', 'i']
letras = [consonantes, vocales]
letras2 = [['b', 'c', 'd', 'f'], ['a', 'e', 'i']]
Aparentemente letras
y letras2
son lo mismo.
print('letras =', letras)
print('letras2 =', letras2)
print(letras == letras2)
letras = [['b', 'c', 'd', 'f'], ['a', 'e', 'i']]
letras2 = [['b', 'c', 'd', 'f'], ['a', 'e', 'i']]
True
Pero fíjate en el resultado de manipular la lista vocales
sin tocar
en absoluto la lista letras
.
vocales.append('o')
print('letras =', letras)
print('letras2 =', letras2)
print(letras == letras2)
letras = [['b', 'c', 'd', 'f'], ['a', 'e', 'i', 'o']]
letras2 = [['b', 'c', 'd', 'f'], ['a', 'e', 'i']]
False
Python no guarda el contenido de las listas consonantes
y
vocales
como elementos de letras
, sino que guarda una referencia
al objeto original, que puede manipularse afectando a todas las
variables que contienen referencias a esas listas.
Podemos ver que se trata de objetos diferentes empleando la función
id
o bien con
PythonTutor.
print(id(letras), id(letras2))
912297503944 912316674440
Esto tiene más implicaciones de las que vemos a primera vista, porque el paso de parámetros no es más que un caso particular de todo esto.
def f(lista):
lista[1].append('u')
f(letras2)
print(letras2)
[['b', 'c', 'd', 'f'], ['a', 'e', 'i', 'u']]
Hasta ahora cuando pasábamos un valor a una función se trataba de una copia, que podía manipular a su antojo sin afectar al programa que llamaba. Por ejemplo:
def cifras(n):
while n > 0:
print(n%10)
n //= 10
n = 1985
cifras(n)
print(n)
5
8
9
1
1985
Al pasar una lista se está pasando una referencia al objeto
correspondiente. Al tratarse de un objeto mutable la función puede
devolver resultados a la función que llama sin ni siquiera usar
return
. Nunca hagas esto.
def cifras(n, lista): # Nunca hagas esto
while n > 0:
lista.append(n%10)
n //= 10
n = []
cifras(1985, n)
print(n)
[5, 8, 9, 1]
Recuerda que programas para que otros lean tus programas. Si escondes el valor de retorno solo estás dificultando la lectura.
def cifras(n):
lista = []
while n > 0:
lista.append(n%10)
n //= 10
return lista
print(cifras(1985))
[5, 8, 9, 1]
6.2. Slicing en listas¶
Se llama slicing (partir en rodajas) a las operaciones que seleccionan una parte de la lista, generando una nueva lista en principio más pequeña. Familiarízate con las operaciones de slicing, son las más frecuentes y no solo en listas.
Veamos algunos ejemplos.
L1 = list(range(10))
L2 = list(range(10))
print(L1)
print(L2)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Cuando se selecciona una parte de la lista el resultado es una lista. En cambio si se usa un índice concreto (operación de indexación) el resultado es un elemento concreto. Eso es muy importante para manipular la lista. Veamos dos ejemplos en los que sustituimos el segundo elemento.
En el primer caso usamos indexación, metemos como segundo elemento el
resultado de la operación de slicing L1[5:]
. Es decir, a partir del
elemento 5.
En el segundo caso sustituimos la lista L2[1:1]
por la lista
L2[5:]
. Aunque L2[1:1]
solo tenga un elemento sigue siendo una
lista, no un elemento. Por eso al sustituir una lista por otra estamos
insertando.
L1[1] = L1[5:]
L2[1:1] = L2[5:]
print(L1)
print(L2)
[0, [5, 6, 7, 8, 9], 2, 3, 4, 5, 6, 7, 8, 9]
[0, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Un ejercicio interesante es practicar slicing para seleccionar las distintas partes de un Sudoku.
sudoku = [
[4,9,1,3,6,2,8,7,5],
[5,2,6,8,7,1,4,9,3],
[7,8,3,9,5,4,6,1,2],
[2,3,9,4,1,7,5,8,6],
[1,5,7,6,3,8,9,2,4],
[6,4,8,2,9,5,7,3,1],
[8,7,2,1,4,6,3,5,9],
[9,1,4,5,8,3,2,6,7],
[3,6,5,7,2,9,1,4,8]]
Practica con filas, y con elementos de una fila. Pero hasta que domines las list comprehensions que se cuentan más adelante no serás capaz de seleccionar los cuadrantes.
6.3. Métodos de una lista¶
Familiarízate con los métodos de las listas. Se utilizan muchísimo.
L = list(range(10))
Añadir un elemento al final.
L.append(4)
print(L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4]
Contar todas las apariciones del elemento 4
.
print(L.count(4))
2
Insertar el elemento 80
en la posición 3
.
L.insert(3, 80)
print(L)
[0, 1, 2, 80, 3, 4, 5, 6, 7, 8, 9, 4]
Añadir al final los elementos de otra lista.
L.extend([2,3,4])
print(L)
[0, 1, 2, 80, 3, 4, 5, 6, 7, 8, 9, 4, 2, 3, 4]
Eliminar la primera ocurrencia del elemento 4
.
L.remove(4)
print(L)
[0, 1, 2, 80, 3, 5, 6, 7, 8, 9, 4, 2, 3, 4]
Imprimir la posición del primer elemento de valor 4
.
print(L.index(4))
10
Eliminar el último de la lista devolviendo su valor. También se puede indicar una posición para eliminar uno cualquiera de la lista.
print(L.pop())
4
Ordenar los elementos de la lista en orden creciente.
L.sort()
print(L)
[0, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 80]
Invertir el orden de todos los elementos de la lista.
L.reverse()
print(L)
[80, 9, 8, 7, 6, 5, 4, 3, 3, 2, 2, 1, 0]
6.4. Clonado de listas¶
Al modificar el contenido de la lista se puede afectar al recorrido de la lista. Por ejemplo, considera la siguiente función:
def borraDuplicados(L1, L2): # ¡OJO! ¡Esta función es incorrecta!
'''Asume que L1 y L2 son listas.
Elimina todos los elementos de L1 que estén presentes en L2.'''
for e in L1:
if e in L2:
L1.remove(e)
Veamos un posible uso de la función.
L1 = [1,2,3,4]
L2 = [1,2,5,6]
borraDuplicados(L1, L2)
print('L1 =', L1)
L1 = [2, 3, 4]
¡Sorpresa! El número 2 está presente en L2
pero no es eliminado de
L1
. ¿Qué ha pasado?
El motivo es que el recorrido de la lista se realiza internamente con un
contador que va desde el 0 (primer elemento) hasta len(L1)
(sin
incluirlo). En la primera iteración comprueba el elemento 0 y descubre
que es un duplicado. Por tanto lo elimina, pero al eliminarlo el primer
elemento deja de existir y su lugar es ocupado por el segundo elemento.
El bucle for
no tiene forma de saber que se ha cambiado el orden de
los elementos y sigue por donde iba, por el segundo elemento. Pero el
que ahora ocupa el segundo lugar es el que antes era el tercero. Se ha
saltado el 2
.
La lección a recordar es que la mutación de una lista invalida los iteradores. Todos los ``for`` que recorran la lista y que se estén ejecutando en el momento de la mutación dejan de tener sentido.
Por tanto el recorrido debe separarse de la mutación, debe hacerse sobre objetos distintos. La forma más sencilla es clonando la lista. Es decir, creando otra lista con los mismos elementos. Y eso ya sabemos hacerlo:
def borraDuplicados(L1, L2):
'''Asume que L1 y L2 son listas.
Elimina todos los elementos de L1 que estén presentes en L2.'''
for e in L1[:]:
if e in L2:
L1.remove(e)
La expresión L1[:]
es una nueva lista que contiene todos los
elementos de L1
. Ahora el recorrido se hace sobre esa nueva lista,
mientras que la operación de mutación remove
se realiza sobre la
lista L1
original.
L1 = [1,2,3,4]
L2 = [1,2,5,6]
borraDuplicados(L1, L2)
print('L1 =', L1)
L1 = [3, 4]
El mismo resultado se obtiene con la llamada a la función list(L1)
.
Crea una nueva lista con los elementos de la que se pasa como argumento.
def borraDuplicados(L1, L2):
'''Asume que L1 y L2 son listas.
Elimina todos los elementos de L1 que estén presentes en L2.'''
for e in list(L1):
if e in L2:
L1.remove(e)
Ambas opciones son perfectamente razonables en un programa Python. Cuál usar es un tema de gusto personal.
L1 = [1,2,3,4]
L2 = [1,2,5,6]
borraDuplicados(L1, L2)
print('L1 =', L1)
L1 = [3, 4]
¿Y si los elementos de la lista son a su vez mutables? Volvemos a tener el mismo problema. Al copiar los elementos cada uno de ellos debe crearse una nueva copia de su contenido. Veamos un ejemplo.
frutas = [ 'pera', 'manzana', 'naranja' ]
verduras = [ 'tomate', 'apio', 'puerro' ]
productos = [ frutas, verduras ]
productos2 = productos[:]
frutas.append('melon')
print(productos2)
[['pera', 'manzana', 'naranja', 'melon'], ['tomate', 'apio', 'puerro']]
La forma más sencilla de realizar copias de objetos complejos es
mediante la biblioteca copy
.
from copy import deepcopy
productos2 = deepcopy(productos)
verduras.append('calabaza')
print(productos2)
[['pera', 'manzana', 'naranja', 'melon'], ['tomate', 'apio', 'puerro']]
Ahora las dos estructuras de datos son completamente independientes y al alterar una o parte de ella no notamos cambio alguno en la otra.
6.5. Comprensiones de listas¶
Una construcción muy importante en Python es la denominada list comprehension o comprensión de lista. Se trata de una notación compacta para generar listas (u otros contenedores) cuyos elementos se puedan escribir en forma de expresiones con los elementos de otra lista.
Por ejemplo, una lista con los primeros 10 cuadrados de números naturales.
[ x**2 for x in range(1,11) ]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
También se puede añadir una condición que actúa como filtro. Es decir,
de los elementos generados solo los que cumplen la condición se incluyen
en la lista. Por ejemplo, tenemos la función os.listdir
que nos dice
el contenido de una carpeta. Veamos que pasa si la llamo en el
directorio de los cuadernos de la asignatura.
import os
os.listdir('.')
['.ipynb_checkpoints',
'BisectionSearch.ipynb',
'Datos AEMET.ipynb',
'Examen ordinario.ipynb',
'ExamenExtraordinario.ipynb',
'Gráficas.ipynb',
'lab-01.ipynb',
'lab-02.ipynb',
'lab-03.ipynb',
'Navegación web.ipynb',
'Paseo.ipynb',
'Problema de las 8 damas.ipynb',
'Procesamiento de XML.ipynb',
'Prueba de Progreso 1ºA.ipynb',
'Prueba de Progreso 1ºB.ipynb',
'Prueba de Progreso 1ºC.ipynb',
'python-00.ipynb',
'python-01.ipynb',
'python-02.ipynb',
'python-04-ejercicios.ipynb',
'python-04.ipynb',
'python-05-06-07.ipynb',
'python-09.ipynb',
'python-10.ipynb',
'python-14.ipynb',
'python-files.ipynb',
'Resumen.ipynb',
'Sudoku 2.ipynb',
'Sudoku 3.ipynb',
'Sudoku.ipynb',
'Untitled.ipynb',
'Untitled1.ipynb',
'Untitled2.ipynb']
Hay archivos que acaban en .ipynb
que son cuadernos y otros que no.
Hay archivos que corresponden al laboratorio, otros que son ejemplos no
relacionados con un tema concreto y otros que son relativos a un tema
concreto. Por ejemplo, los que empiezan por python-
y acaban en
.ipynb
son cuadernos relativos a un tema. Vamos a generar una lista
por comprensión.
[ f for f in os.listdir('.') if f.endswith('.ipynb') and f.startswith('python-') ]
['python-00.ipynb',
'python-01.ipynb',
'python-02.ipynb',
'python-04-ejercicios.ipynb',
'python-04.ipynb',
'python-05-06-07.ipynb',
'python-09.ipynb',
'python-10.ipynb',
'python-14.ipynb',
'python-files.ipynb']
Volvamos al ejercicio del Sudoku. Vamos a seleccionar partes de él ahora que sabemos todo acerca de las list comprehensions.
sudoku = [
[4,9,1,3,6,2,8,7,5],
[5,2,6,8,7,1,4,9,3],
[7,8,3,9,5,4,6,1,2],
[2,3,9,4,1,7,5,8,6],
[1,5,7,6,3,8,9,2,4],
[6,4,8,2,9,5,7,3,1],
[8,7,2,1,4,6,3,5,9],
[9,1,4,5,8,3,2,6,7],
[3,6,5,7,2,9,1,4,8]]
Seleccionemos el cuadrante superior izquierdo.
[s[:3] for s in sudoku[:3]]
[[4, 9, 1], [5, 2, 6], [7, 8, 3]]
Ahora el inferior derecho.
[s[6:] for s in sudoku[6:]]
[[3, 5, 9], [2, 6, 7], [1, 4, 8]]
Ahora el del medio.
[s[3:6] for s in sudoku[3:6]]
[[4, 1, 7], [6, 3, 8], [2, 9, 5]]
Ahora la tercera columna.
[ s[2] for s in sudoku ]
[1, 6, 3, 9, 7, 8, 2, 4, 5]
Practica tú. Haz por ejemplo que los cuadrantes aparezcan como una lista simple en lugar de una lista de listas.
Piénsalo un poco por tí mismo y si no llegas a la solución sigue leyendo. A partir de aquí se pede considerar un uso avanzado de las list comprehensions. No te preocupes si no las entiendes.
Por ejemplo, el cuadrante superior izquierdo:
[ i for s in sudoku[:3] for i in s[:3] ]
[4, 9, 1, 5, 2, 6, 7, 8, 3]
El cuadrante inferior derecho:
[ i for s in sudoku[6:] for i in s[6:] ]
[3, 5, 9, 2, 6, 7, 1, 4, 8]
El del medio:
[ i for s in sudoku[3:6] for i in s[3:6] ]
[4, 1, 7, 6, 3, 8, 2, 9, 5]
Piensa ahora una función para devolver el cuadrante (x, y) siendo x e y números entre 0 y 2.
def cuadrante(sudoku, x, y):
return [ i for s in sudoku[3*y:][:3] for i in s[3*x:][:3] ]
Veamos para probarlo los cuadrantes centrales.
print(cuadrante(sudoku, 0, 1))
print(cuadrante(sudoku, 1, 1))
print(cuadrante(sudoku, 2, 1))
[2, 3, 9, 1, 5, 7, 6, 4, 8]
[4, 1, 7, 6, 3, 8, 2, 9, 5]
[5, 8, 6, 9, 2, 4, 7, 3, 1]