Tuplas

Las Tuplas son inmutables

Una tupla1 es una secuencia de valores similar a una lista. Los valores guardados en una tupla pueden ser de cualquier tipo, y son indexados por números enteros. La principal diferencia es que las tuplas son inmutables. Las tuplas además son comparables y dispersables (hashables) de modo que las listas de tuplas se pueden ordenar y también usar tuplas como valores para las claves en diccionarios de Python.

Sintácticamente, una tupla es una lista de valores separados por comas:

>>> t = 'a', 'b', 'c', 'd', 'e'

Aunque no es necesario, es común encerrar las tuplas entre paréntesis para ayudarnos a identificarlas rápidamente cuando revisemos código de Python:

>>> t = ('a', 'b', 'c', 'd', 'e')

Para crear una tupla con un solo elemento, es necesario incluir una coma al final:

>>> t1 = ('a',)
>>> type(t1)
<type 'tuple'>

Sin la coma, Python considera ('a') como una expresión con una cadena entre paréntesis que es evaluada como de tipo cadena (string):

>>> t2 = ('a')
>>> type(t2)
<type 'str'>

Otra forma de construir una tupla es utilizando la función interna tuple. Sin argumentos, ésta crea una tupla vacía:

>>> t = tuple()
>>> print(t)
()

Si el argumento es una secuencia (cadena, lista, o tupla), el resultado de la llamada a tuple es una tupla con los elementos de la secuencia:

>>> t = tuple('altramuces')
>>> print(t)
('a', 'l', 't', 'r', 'a', 'm', 'u', 'c', 'e', 's')

Dado que tuple es el nombre de un constructor, debería evitarse su uso como nombre de variable.

La mayoría de los operadores de listas también funcionan en tuplas. El operador corchete indexa un elemento:

>>> t = ('a', 'b', 'c', 'd', 'e')
>>> print(t[0])
'a'

Y el operador de rebanado (slice) selecciona un rango de elementos.

>>> print(t[1:3])
('b', 'c')

Pero si se intenta modificar uno de los elementos de la tupla, se produce un error:

>>> t[0] = 'A'
TypeError: object doesn't support item assignment

No se puede modificar los elementos de una tupla, pero sí se puede reemplazar una tupla por otra:

>>> t = ('A',) + t[1:]
>>> print(t)
('A', 'b', 'c', 'd', 'e')

Comparación de tuplas

Los operadores de comparación funcionan con tuplas y otras secuencias. Python comienza comparando el primer elemento de cada secuencia. Si ambos elementos son iguales, pasa al siguiente elemento y así sucesivamente, hasta que encuentra elementos diferentes. Los elementos subsecuentes no son considerados (aunque sean muy grandes).

>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True

La función sort funciona de la misma manera. Ordena inicialmente por el primer elemento, pero en el caso de que ambos elementos sean iguales, ordena por el segundo elemento, y así sucesivamente.

Esta característica se presta a un patrón de diseño llamado DSU, que

Decorate (Decora)
una secuencia, construyendo una lista de tuplas con uno o más índices ordenados precediendo los elementos de la secuencia,
Sort (Ordena)
la lista de tuplas utilizando la función interna sort, y
Undecorate (Quita la decoración)
extrayendo los elementos ordenados de la secuencia.

Por ejemplo, suponiendo una lista de palabras que se quieren ordenar de la más larga a la más corta:

txt = 'Pero qué luz se deja ver allí'
palabras = txt.split()
t = list()
for palabra in palabras:
    t.append((len(palabra), palabra))

t.sort(reverse=True)

res = list()
for longitud, palabra in t:
    res.append(palabra)

print(res)

# Código: https://es.py4e.com/code3/soft.py

El primer bucle genera una lista de tuplas, donde cada tupla es una palabra precedida por su longitud.

sort compara el primer elemento (longitud) primero, y solamente considera el segundo elemento para desempatar. El argumento clave reverse=True indica a sort que debe ir en orden decreciente.

El segundo bucle recorre la lista de tuplas y construye una lista de palabras en orden descendente según la longitud. Las palabras de cuatro letras están ordenadas en orden alfabético inverso, así que “deja” aparece antes que “allí” en la siguiente lista.

La salida del programa es la siguiente:

['deja', 'allí', 'Pero', 'ver', 'qué', 'luz', 'se']

Por supuesto, la línea pierde mucho de su impacto poético cuando se convierte en una lista de Python y se almacena en orden descendente según la longitud de las palabras.

Asignación de tuplas

Una de las características sintácticas únicas del lenguaje Python es la capacidad de tener una tupla en el lado izquierdo de una sentencia de asignación. Esto permite asignar más de una variable a la vez cuando hay una secuencia del lado izquierdo.

En este ejemplo tenemos una lista de dos elementos (la cual es una secuencia) y asignamos el primer y segundo elementos de la secuencia a las variables x y y en una única sentencia.

>>> m = [ 'pásalo', 'bien' ]
>>> x, y = m
>>> x
'pásalo'
>>> y
'bien'
>>>

No es magia, Python traduce aproximadamente la sintaxis de asignación de la tupla de este modo::2

>>> m = [ 'pásalo', 'bien' ]
>>> x = m[0]
>>> y = m[1]
>>> x
'pásalo'
>>> y
'bien'
>>>

Estilísticamente, cuando se utiliza una tupla en el lado izquierdo de la asignación, se omiten los paréntesis, pero lo que se muestra a continuación es una sintaxis igualmente válida:

>>> m = [ 'pásalo', 'bien' ]
>>> (x, y) = m
>>> x
'pásalo'
>>> y
'bien'
>>>

Una aplicación particularmente ingeniosa de asignación con tuplas permite intercambiar los valores de dos variables en una sola sentencia:

>>> a, b = b, a

Ambos lados de la sentencia son tuplas, pero el lado izquierdo es una tupla de variables; el lado derecho es una tupla de expresiones. Cada valor en el lado derecho es asignado a su respectiva variable en el lado izquierdo. Todas las expresiones en el lado derecho son evaluadas antes de realizar cualquier asignación.

El número de variables en el lado izquierdo y el número de valores en el lado derecho deben ser iguales:

>>> a, b = 1, 2, 3
ValueError: too many values to unpack

Generalizando más, el lado derecho puede ser cualquier tipo de secuencia (cadena, lista, o tupla). Por ejemplo, para dividir una dirección de e-mail en nombre de usuario y dominio, se podría escribir:

>>> dir = '[email protected]'
>>> nombreus, dominio = dir.split('@')

El valor de retorno de split es una lista con dos elementos; el primer elemento es asignado a nombreus, el segundo a dominio.

>>> print(nombreus)
monty
>>> print(dominio)
python.org

Diccionarios y tuplas

Los diccionarios tienen un método llamado items que retorna una lista de tuplas, donde cada tupla es un par clave-valor:

>>> d = {'a':10, 'b':1, 'c':22}
>>> t = list(d.items())
>>> print(t)
[('b', 1), ('a', 10), ('c', 22)]

Como sería de esperar en un diccionario, los elementos no tienen ningún orden en particular.

Aun así, puesto que la lista de tuplas es una lista, y las tuplas son comparables, ahora se puede ordenar la lista de tuplas. Convertir un diccionario en una lista de tuplas es una forma de obtener el contenido de un diccionario ordenado según sus claves:

>>> d = {'a':10, 'b':1, 'c':22}
>>> t = list(d.items())
>>> t
[('b', 1), ('a', 10), ('c', 22)]
>>> t.sort()
>>> t
[('a', 10), ('b', 1), ('c', 22)]

La nueva lista está ordenada en orden alfabético ascendente de acuerdo al valor de sus claves.

Asignación múltiple con diccionarios

La combinación de items, asignación de tuplas, y for, produce un buen patrón de diseño de código para recorrer las claves y valores de un diccionario en un único bucle:

for clave, valor in list(d.items()):
    print(valor, clave)

Este bucle tiene dos variables de iteración, debido a que items retorna una lista de tuplas y clave, valor es una asignación en tupla que itera sucesivamente a través de cada uno de los pares clave-valor del diccionario.

Para cada iteración a través del bucle, tanto clave y valor van pasando al siguiente par clave-valor del diccionario (todavía en orden de dispersión).

La salida de este bucle es:

10 a
1 b
22 c

De nuevo, las claves están en orden de dispersión (es decir, ningún orden en particular).

Si se combinan esas dos técnicas, se puede imprimir el contenido de un diccionario ordenado por el valor almacenado en cada par clave-valor.

Para hacer esto, primero se crea una lista de tuplas donde cada tupla es (valor, clave). El método items dará una lista de tuplas (clave, valor), pero esta vez se pretende ordenar por valor, no por clave. Una vez que se ha construido la lista con las tuplas clave-valor, es sencillo ordenar la lista en orden inverso e imprimir la nueva lista ordenada.

>>> d = {'a':10, 'b':1, 'c':22}
>>> l = list()
>>> for clave, valor in d.items() :
...     l.append( (valor, clave) )
...
>>> l
[(10, 'a'), (1, 'b'), (22, 'c')]
>>> l.sort(reverse=True)
>>> l
[(22, 'c'), (10, 'a'), (1, 'b')]
>>>

Al construir cuidadosamente la lista de tuplas para tener el valor como el primer elemento de cada tupla, es posible ordenar la lista de tuplas y obtener el contenido de un diccionario ordenado por valor.

Las palabras más comunes

Volviendo al ejemplo anterior del texto de Romeo y Julieta, Acto 2, Escena 2, podemos mejorar nuestro programa para hacer uso de esta técnica para imprimir las diez palabras más comunes en el texto, como se ve a continuación:

import string
manejador = open('romeo-full.txt')
contadores = dict()
for linea in manejador:
    linea = linea.translate(str.maketrans('', '', string.punctuation))
    linea = linea.lower()
    palabras = linea.split()
    for palabra in palabras:
        if palabra not in contadores:
            contadores[palabra] = 1
        else:
            contadores[palabra] += 1

# Ordenar el diccionario por valor
lst = list()
for clave, valor in list(contadores.items()):
    lst.append((valor, clave))

lst.sort(reverse=True)

for clave, valor in lst[:10]:
    print(clave, valor)

# Código: https://es.py4e.com/code3/count3.py

La primera parte del programa, la cual lee un archivo y construye un diccionario que mapea cada palabra con la cantidad de veces que se repite esa palabra en el documento, no ha cambiado. Pero en lugar de imprimir simplemente contadores y terminar el programa, ahora construimos una lista de tuplas (val, key) y luego se ordena la lista en orden inverso.

Puesto que el valor está primero, será utilizado para las comparaciones. Si hay más de una tupla con el mismo valor, se tendrá en cuenta el segundo elemento (la clave), de forma que las tuplas cuyo valor es el mismo serán también ordenadas en orden alfabético según su clave.

Al final escribimos un elegante bucle for que hace una iteración con asignación múltiple e imprime las diez palabras más comunes, iterando a través de una parte de la lista (lst[:10]).

Ahora la salida finalmente tiene el aspecto que queríamos para nuestro análisis de frecuencia de palabras.

61 i
42 and
40 romeo
34 to
34 the
32 thou
32 juliet
30 that
29 my
24 thee

El hecho de que este complejo análisis y procesado de datos pueda ser realizado con un programa de Python de 19 líneas fácil de entender, es una razón de por qué Python es una buena elección como lenguaje para explorar información.

Uso de tuplas como claves en diccionarios

Dado que las tuplas son dispersables (hashable) y las listas no, si se quiere crear una clave compuesta para usar en un diccionario, se debe utilizar una tupla como clave.

Usaríamos por ejemplo una clave compuesta si quisiéramos crear un directorio telefónico que mapea pares appellido, nombre con números telefónicos. Asumiendo que hemos definido las variables apellido, nombre, y número, podríamos escribir una sentencia de asignación de diccionario como sigue:

directorio[apellido,nombre] = numero

La expresión entre corchetes es una tupla. Podríamos utilizar asignación de tuplas en un bucle for para recorrer este diccionario.

for apellido, nombre in directorio:
    print(nombre, apellido, directorio[apellido,nombre])

Este bucle recorre las claves en directorio, las cuales son tuplas. Asigna los elementos de cada tupla a apellido y nombre, después imprime el nombre y el número telefónico correspondiente.

Secuencias: cadenas, listas, y tuplas - ¡Dios mío!

Me he enfocado en listas de tuplas, pero casi todos los ejemplos de este capítulo funcionan también con listas de listas, tuplas de tuplas, y tuplas de listas. Para evitar enumerar todas las combinaciones posibles, a veces es más sencillo hablar de secuencias de secuencias.

En muchos contextos, los diferentes tipos de secuencias (cadenas, listas, y tuplas) pueden intercambiarse. Así que, ¿cómo y por qué elegir uno u otro?

Para comenzar con lo más obvio, las cadenas están más limitadas que otras secuencias, debido a que los elementos tienen que ser caracteres. Además, son inmutables. Si necesitas la capacidad de cambiar los caracteres en una cadena (en vez de crear una nueva), quizá prefieras utilizar una lista de caracteres.

Las listas son más comunes que las tuplas, principalmente porque son mutables. Pero hay algunos casos donde es preferible utilizar tuplas:

  1. En algunos contextos, como una sentencia return, resulta sintácticamente más simple crear una tupla que una lista. En otros contextos, es posible que prefieras una lista.

  2. Si quieres utilizar una secuencia como una clave en un diccionario, debes usar un tipo inmutable como una tupla o una cadena.

  3. Si estás pasando una secuencia como argumento de una función, el uso de tuplas reduce la posibilidad de comportamientos inesperados debido a la creación de alias.

Dado que las tuplas son inmutables, no proporcionan métodos como sort y reverse, que modifican listas ya existentes. Sin embargo, Python proporciona las funciones internas sorted y reversed, que toman una secuencia como parámetro y devuelven una secuencia nueva con los mismos elementos en un orden diferente.

Depuración

Las listas, diccionarios y tuplas son conocidas de forma genérica como estructuras de datos; en este capítulo estamos comenzando a ver estructuras de datos compuestas, como listas de tuplas, y diccionarios que contienen tuplas como claves y listas como valores. Las estructuras de datos compuestas son útiles, pero también son propensas a lo que yo llamo errores de modelado; es decir, errores causados cuando una estructura de datos tiene el tipo, tamaño o composición incorrecto, o quizás al escribir una parte del código se nos olvidó cómo era el modelado de los datos y se introdujo un error. Por ejemplo, si estás esperando una lista con un entero y recibes un entero solamente (no en una lista), no funcionará.

Glosario

comparable
Un tipo en el cual un valor puede ser revisado para ver si es mayor que, menor que, o igual a otro valor del mismo tipo. Los tipos que son comparables pueden ser puestos en una lista y ordenados.
estructura de datos
Una collección de valores relacionados, normalmente organizados en listas, diccionarios, tuplas, etc.
DSU
Abreviatura de “decorate-sort-undecorate (decorar-ordenar-quitar la decoración)”, un patrón de diseño que implica construir una lista de tuplas, ordenarlas, y extraer parte del resultado.
reunir
La operación de tratar una secuencia como una lista de argumentos.
hashable (dispersable)
Un tipo que tiene una función de dispersión. Los tipos inmutables, como enteros, flotantes y cadenas son dispersables (hashables); los tipos mutables como listas y diccionarios no lo son.
dispersar
La operación de tratar una secuencia como una lista de argumentos.
modelado (de una estructura de datos)
Un resumen del tipo, tamaño, y composición de una estructura de datos.
singleton
Una lista (u otra secuencia) con un único elemento.
tupla
Una secuencia inmutable de elementos.
asignación por tuplas
Una asignación con una secuencia en el lado derecho y una tupla de variables en el izquierdo. El lado derecho es evaluado y luego sus elementos son asignados a las variables en el lado izquierdo.

Ejercicios

Ejercicio 1: Revisa el programa anterior de este modo: Lee y analiza las líneas “From” y extrae las direcciones de correo. Cuenta el número de mensajes de cada persona utilizando un diccionario.

Después de que todos los datos hayan sido leídos, imprime la persona con más envíos, creando una lista de tuplas (contador, email) del diccionario. Después ordena la lista en orden inverso e imprime la persona que tiene más envíos.

Línea de ejemplo:
From [email protected] Sat Jan  5 09:14:16 2008

Ingresa un nombre de archivo: mbox-short.txt
[email protected] 5

Ingresa un nombre de archivo: mbox.txt
[email protected] 195

Ejercicio 2: Este programa cuenta la distribución de la hora del día para cada uno de los mensajes. Puedes extraer la hora de la línea “From” buscando la cadena horaria y luego dividiendo la cadena en partes utilizando el carácter colon. Una vez que hayas acumulado las cuentas para cada hora, imprime las cuentas, una por línea, ordenadas por hora tal como se muestra debajo.

python timeofday.py
Ingresa un nombre de archivo: mbox-short.txt
04 3
06 1
07 1
09 2
10 3
11 6
14 1
15 2
16 4
17 2
18 1
19 1

Ejercicio 3: Escribe un programa que lee un archivo e imprime las letras en order decreciente de frecuencia. El programa debe convertir todas las entradas a minúsculas y contar solamente las letras a-z. El programa no debe contar espacios, dígitos, signos de puntuación, o cualquier cosa que no sean las letras a-z. Encuentra ejemplos de texto en idiomas diferentes, y observa cómo la frecuencia de letras es diferente en cada idioma. Compara tus resultados con las tablas en https://es.wikipedia.org/wiki/Frecuencia_de_aparici%C3%B3n_de_letras.


  1. Dato curioso: La palabra “tuple” proviene de los nombres dados a secuencias de números de distintas longitudes: simple, doble, triple, cuádruple, quíntuple, séxtuple, séptuple, etc.↩︎

  2. Python no traduce la sintaxis literalmente. Por ejemplo, si se trata de hacer esto con un diccionario, no va a funcionar como se podría esperar.↩︎


Si encuentras un error en este libro, siéntete libre de enviarme una solución usando Github.