Mostrando entradas con la etiqueta list(). Mostrar todas las entradas
Mostrando entradas con la etiqueta list(). Mostrar todas las entradas

viernes, 3 de mayo de 2024

Descifrador de códigos.

 Basado en el proyecto de la Raspberripy Org "Codebreakers".

¿Qué es lo que haremos en este post?


Analizaremos un gráfico para intentar descifrar un código, mientras aprendemos sobre listas y funciones en Python.

Que haremos:

- Definir una función que acepte una serie de parámetros y devuelva un valor.
- Usar los bucles "for" y "while" para repetir tareas.
- Crear un gráfico para mostrar la frecuencia de unos datos.

El programa que crearemos puede codificar o descodificar un mensaje secreto desde un archivo *.txt adjunto, o también podremos analizar la frecuencia de las letras en un mensaje. Estos datos se mostrarán en un gráfico de frecuencias, que mostrará con que frecuencia cada letra aparece en el mensaje secreto.

Nos basaremos en el cifrado Atbash. Este cifrado es uno de los más antiguos que se conocen en el mundo. Originariamente desarrollado en el idioma hebreo, utiliza el alfabeto y lo relaciona en orden inverso para crear el mensaje secreto. El nombre deriva de la primera, última, segunda y penúltima letras hebreas: Aleph, Taw, Bet y Shin.

Cifrando el alfabeto.


Para empezar vamos a crear un diccionario para tus letras cifradas.  El programa comenzará con dos estructuras de datos. La primera es una lista de todas las letras que existen en nuestro alfabeto y la segunda el un diccionario para el código. Para ahorrar teclear código, puedes crear la lista desde una cadena de texto usando la función list().

Empieza el programa con el siguiente código:

#Estructura de datos
#Lista creada desde una cadena de texto
ALFABETO = list(" abcdefghijklmnñopqrstuvwxyz ")
codigo = {}
La lista ALFABETO, como puedes ver, contiene un espacio al principio y al final de las letras. Esto lo hemos hecho para preservar los espacios que existan en el texto del mensaje. Sin embargo en una codificación fuerte esto no debería ser así, ya que hace que los mensajes sean más fáciles de decodificar. En este proyecto hemos mantenido los espacios para que que pueda ver más fácilmente el funcionamiento del programa.

Creando una nueva lista con el alfabeto invertido.


Necesitamos crear una nueva lista pero con el alfabeto, pero invertido. Puedes usar la funcion list() de nuevo para ayudarte con esto. Puedes usar la función reversed() para invertir el orden de una lista existente.

Crea la función crear_codigo. A continuación crea una lista que almacene el alfabeto pero al revés en una lista.

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    inversa = list(reversed(ALFABETO))


Codificando el alfabeto.


"Codificar" es cuando conviertes datos de un formato a otro. Ahora tienes dos listas. Una contiene la lista del alfabeto en su orden normal y la otra en su orden inverso. Vamos a usar ambas para rellenar un diccionario. La llave almacenará almacenará el alfabeto en su orden normal y el valor correspondiente guardará el diccionario al revés.

El diccionario codigo es muy importante porque lo usaremos para hacer coincidir cada letra del mensaje usando las llaves y sus correspondientes valores.

Dentro de la función crear_codigo, rellena el diccionario usando los datos de ambas listas. Usa un bucle "for" para obtener la longitud de la lista del alfabeto y completar el diccionario con los datos. 

La función len() se usa para obtener la longitud de un objeto, como puede ser una lista. Aquí se usar para que el bucle "for" itere tantas veces como elementos tiene la lista "ALFABETO", es decir su longitud. 

def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

Ahora tenemos un problema a solventar. En el idioma español existen los acentos. Para usarlos en nuestro código tenemos que pasar las letras acentuadas como letras normales ya que si no no se procesarían al no estar en el diccionario. Para solventarlo añadimos el diccionario caracteres_especiales y luego añadimos sus elementos al diccionario codigo.

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.
    
    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}
    
    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)   

Crear una función de entrada "main.py" es útil para llamar a las funciones que se necesitan cuando comienza el programa. Creemos una función main.py que llame a la función crear_codigo. 

# Comienzo
def main():
    crear_codigo()

main()

Probar y depurar el código.


Para probar que el diccionario se ha creado correctamente, vamos a imprimir el diccionario completo. El programa hasta ahora tiene que verse de esta forma:

# Crear el código Atbash invirtiendo el alfabeto
def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}

    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)
    
    print(codigo)

#Estructura de datos
#Lista creada desde una cadena de texto
ALFABETO = list("abcdefghijklmnñopqrstuvwxyz")
codigo = {}

# Comienzo
def main():
    crear_codigo()

main()
Ejecuta el código para ver si el diccionario se ha creado correctamente. Deberías ver un patrón que empieza con la letra "a" emparejada con la letra "z" y la letra "b" emparejada con la letra "y" y así sucesivamente.

SALIDA:

{' ': ' ', 'a': 'z', 'b': 'y', 'c': 'x', 'd': 'w', 'e': 'v', 'f': 'u', 'g': 't',
 'h': 's', 'i': 'r', 'j': 'q', 'k': 'p', 'l': 'o', 'm': 'ñ', 'n': 'n', 'ñ': 'm',
 'o': 'l', 'p': 'k', 'q': 'j', 'r': 'i', 's': 'h', 't': 'g', 'u': 'f', 'v': 'e',
 'w': 'd', 'x': 'c', 'y': 'b', 'z': 'a', 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o',
 'ú': 'u'}

Codificar un mensaje.


En este apartado crearemos una función que pueda tomar un texto, lo transforma y te lo devuelva cifrado. 

Empezaremos comentando el print que usamos para ver si funcionaba nuestro código.

def crear_codigo():
    # reversa del ALFABETO
    inversa = list(reversed(ALFABETO))
    
    for i in range(len(ALFABETO)): # Longitud de una lista
        codigo[ALFABETO[i]] = inversa[i]
        # Rellena el alfabeto con una letra y su correspondiente
        # letra códificada.

    # Lista de caracteres especiales y sus equivalentes en el alfabeto normal
    caracteres_especiales = {'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u'}

    # Agregar caracteres especiales al diccionario de códigos
    codigo.update(caracteres_especiales)

    #print(codigo) 

Definiendo la función Atbash.

Vamos a crear una función para codificar un texto usando el cifrado Atbash. Empieza definiendo la función Atbash, con un parámetro llamado texto. Los parámetros te permiten pasar valores para que sean usados en las funciones. 

def atbash(texto):

Convirtiendo el texto a minúsculas.

Lo primero que necesita la función es convertir el texto que le pasemos a minúsculas. Necesitamos crear una nueva variable, la podemos llamar salida, para guardar el mensaje codificado.

def atbash(texto):
    """Codifica o decodifica una línea de texto.
    Atbash es símetrico.
    """
    texto = texto.lower() # Convierte el texto en minúsculas.
    salida = ""

Codificando el texto.

Lo próximo que haremos será codificar el texto que habremos pasado a la función. Utilizaremos un bucle for para iterar sobre cada letra del texto y convertirla en una letra codificada usando el diccionario codigo. Finalmente devolveremos el mensaje codificado.

def atbash(texto):
    """Codifica o decodifica una línea de texto.
    Atbash es símetrico.
    """
    texto = texto.lower() # Convierte el texto en minúsculas.
    salida = ""
    
    for letra in texto:
        if letra in codigo:
            salida += codigo[letra]
    
    return salida

Probar y depurar.

Ahora que tenemos la función vamos a probarla codificando algo de texto y ver que funciona. Localiza la función main() y añade una llamada a la función atbash(). Podemos pasar el texto "Codificar es fácil"

Si todo ha ido bien la salida del programa sería:

>>> %Run main.py
xlwrurxzi vh uaxro 

Creando un menú.


Ahora lo que haremos será crear un menú para que el usuario pueda elegir que es lo que quiere hacer. Empezaremos creando una función llamada "menu":

def menu():
    """Crea un menú basado en texto."""
Tu menú necesita un bucle que pregunte al usuario que es lo que quiere hacer hasta que introduzca una opción válida. Tenemos que definir una variable llamada "elección" que de momento no tendrá ningún valor.  Esto permitirá al bucle while ejecutar su primer bucle.

def menu():
    """Crea un menú basado en texto."""
    eleccion = "" # El bucle comienza con una elección no válida.

Usando un bucle while para obtener lo que teclee el usuario.

Lo que queremos es que el bucle se rompa solamente cuando se haya escogido una opción válida. Queremos que el bucle while se siga ejecutando mientras no se escoja una opción válida.

Puedes usar un bucle while para ejecutar un trozo de código mientras una condición se verdadera (True). En nuestro caso vamos a definir que mientras que el usuario no elija la letra "d" o la "f" el bucle se continuará ejecutando. Lo que pretendemos es que si se pulsa una "c" se codifique/decodifique un mensaje que habremos guardado en un archivo de texto y si pulsa una "f" se muestre un gráfico con la frecuencia de las letras en ese mensaje. 

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while elección != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: ''')

Una vez que el usuario ha introducido una elección correcta, el bucle finaliza. A continuación crearemos una sentencia 'if' que ejecutará la función atbash si el usuario introduce una 'c'

Tu decidirás que sucederá cuando el usuario introduzca una 'f' en un paso posterior.

Debajo de la última línea (¡asegurándote de tener todavía una sangría!), escribe:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: ''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            mensaje = "Mi mensaje secreto"
            codigo = atbash(mensaje)
            print(codigo)

Cambia la cadena que dice "Mi mensaje secreto" por el texto que tu quieras. Este texto será el que se codifique o decodifique.

Al final de la función main() teclea menu() para llamar a la función cuando se ejecute el programa:

# Comienzo
def main():
    crear_codigo()
    #print(atbash("Codificar es fácil"))
    menu()
Ejecuta el código. Teclea 'c' y presiona Enter para codificar tu mensaje.

SALIDA.

>>> %Run main.py
Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
ñr ñvnhzqv hvxivgl
En el próximo paso usaremos la función atbash para codificar el contenido de un archivo de texto.

Codificar texto desde un archivo.


Cargar el texto desde un archivo es más eficiente que teclear o copiar un gran texto para pasárselo al programa. Vamos a crear la función obtener_texto(). Esta función tiene un argumento que es el nombre del archivo a cargar. Usamos este argumento para cargar el archivo y pasárselo a la variable texto, que una vez quitados los saltos de línea es devuelto por la función. El código quedaría tal que así:

def obtener_texto(nombre_archivo):
    """Obtiene y devuelve texto desde un archivo."""
    with open(nombre_archivo) as f:
        texto = f.read().replace('\n', " ") # tenemos que eliminar los saltos de línea.
        return texto
La función en el menú necesita codificar el mensaje secreto desde un archivo de texto. Reemplaza tu mensaje secreto con la función obtener_texto(). Así quedaría el código:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c to codificar/decodificar texto,
o f para realizar un análisis de frecuencias: \n''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            print(codigo)
Crea en el mismo directorio un archivo llamado datos.txt, edítalo e introduce en el tu propio mensaje secreto.

Ejecuta tu código para ver si se muestra el mensaje codificado después de pulsar la letra 'c' en el menú.

SALIDA.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
vhgv vh fn ñvnhzqv wv kifvyz kziz evi hr vo xlwrurxzwli wv gvcglh ufnxrlnz xliivxgzñvngv

Decodificando el mensaje.


El cifrado atbash codifica el mensaje utilizando el reverso de las letras del alfabeto, como ya hemos visto. Esto significa que exactamente el mismo código que hemos utilizado para codificar el mensaje vale para decodificarlo. Puedes probarlo, usando el código encriptado. Copialo y pégalo en el archivo datos.txt y vuelve a ejecutar el código de nuevo.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
c
Pasando tu mensaje a través del cifrado...
este es un mensaje de prueba para ver si el codificador de textos funciona correctamente 

Se mostrará la versión descodificada, es decir tu mensaje original.


Crear un analizador de frecuencias.


En este paso vamos a crear una función que nos muestre la frecuencia con la que las letras del alfabeto aparecen en tu texto.

El análisis de frecuencias mide con que frecuencia algo aparece de esta forma podrás buscar patrones de ocurrencia en esos datos. Es posible descifrar cifrados monoalfabeticos (si conoces el idioma en el que se encuentra el mensaje) observando la frecuencia con la que aparece cada letra y comparándolo con las letras más utilizadas en ese idioma. Lo explicaremos en más detalle más adelante.

Empezaremos creando una función que tome tu texto y cuente el número de veces que cada letra del alfabeto aparece en él y luego convierta ese número de apariciones en un porcentaje para que puedas compararlo con la frecuencia de las letras en español.

Vamos al lio. Define una función llamada frecuencia que tome como argumento tu texto, luego convierta el mensaje en minúsculas y lo pase todo a una lista.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
Crea un diccionario llamado frec donde vamos a almacenar el número de veces que aparece cada letra en el texto. Luego recorreremos el texto a través de un bucle for y añadiremos al diccionario el número de veces que aparece cada letra. Para posteriormente calcular las frecuencias almacenamos el número total de palabras en la variable letras_totales.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
    
    frec = {}
    
    for letra in texto:
        frec[letra] = frec.get(letra, 0) + 1
Por ejemplo si la primera letra del texto fuese una 'a' el programa busca si existe una clave en el diccionario con la 'a'. Usamos el método get ya que si existe la clave en el diccionario nos devuelve su valor y si no es así, porque fuese la primera aparición, nos devolverá el número cero en vez de un error.
Cada vez que aparece la letra en el texto le sumamos uno a su valor.

Una vez que ya tengamos el número total de veces que cada letra aparece en el mensaje vamos a calcular su frecuencia  a través de un bucle. No queremos que se tengan en cuenta los espacios en blanco para el calculo de las frecuencias, así que añadimos el condicional if para añadir solo las letras. Añade un return a la función para que podamos usar el diccionario con las frecuencias en cualquier parte del código.

def frecuencia(texto):
    """Calcula la frecuencia de las letras en un texto"""
    texto = list(texto.lower())
    
    frec = {}
        
    for letra in texto:
        if letra != " ":
            frec[letra] = frec.get(letra, 0) + 1
    
    letras_totales = sum(frec.values()) #numero total de letras sin espacios en blanco
        
    for letra in frec:
        frec[letra] = frec[letra] / letras_totales * 100  # Convierte números en porcentajes.
    
    return frec

Extendiendo el menú para incluir la opción 'f'.


Ahora que tenemos una función que calcula la frecuencia de las letras en el mensaje, necesitamos un enlace para ejecutarla a través del menú. Hasta ahora el usuario solo podía escoger la opción 'c' para codificar o decodificar un mensaje. Si tecleaba la letra 'f' no ocurría nada. Vamos a añadir esta opción para mostrar un análisis de frecuencias con la función que hemos creado anteriormente.

Debajo del comando if que verifica si el usuario ha pulsado una 'c',  necesitamos añadir el comando elif para que el usuario pueda escoger la opción 'f' para realizar un análisis de frecuencias e imprimirlas.

Deja una línea en blanco después de la sentencia 'if ' y añade el siguiente código:

def menu():
    """Crea un menú basado en texto."""
    eleccion = ""
    
    while eleccion != 'c' and eleccion != 'f':
        eleccion = input('''Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: \n''')
        
        if eleccion == 'c':
            print("Pasando tu mensaje a través del cifrado...")
            # mensaje = "Mi mensaje secreto"
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            print(codigo)
            
        elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            print(mensaje_frec)
Guarda y ejecuta el código. Presiona la tecla 'f' y deberías ver en la consola una relación de las frecuencias con la que las letras aparecen en tu mensaje ya codificado. Los valores que se muestren seguramente serán diferentes de los mostrados aquí.

>>> %Run main.py
Por favor, introduce c para codificar/decodificar texto,
o f para realizar un análisis de frecuencias: 
f
{'v': 18.91891891891892, 'h': 6.756756756756757, 'g': 6.756756756756757,
 'f': 4.054054054054054, 'n': 6.756756756756757, 'ñ': 2.7027027027027026,
 'z': 9.45945945945946, 'q': 1.3513513513513513, 'w': 5.405405405405405,
 'k': 2.7027027027027026, 'i': 8.108108108108109, 'y': 1.3513513513513513,
 'e': 1.3513513513513513, 'r': 5.405405405405405, 'o': 1.3513513513513513,
 'x': 6.756756756756757, 'l': 6.756756756756757, 'u': 2.7027027027027026,
 'c': 1.3513513513513513}


Analizando la frecuencia mediante un gráfico.


Lo primero que tenemos que hacer es instalar la librería Pygal para mostrar el gráfico de las frecuencias. 

pip3 install pygal

En todos los idiomas, cada letra de su alfabeto tiene una "personalidad" o un conjunto de rasgos cuando se usa en ese idioma. Una de las características que tiene una letra en cualquier idioma es la frecuencia con la que aparece. El análisis de frecuencia es el método para descifrar códigos observando la frecuencia con la que se utilizan las letras en el idioma del código y comparándolo con la frecuencia con la que aparecen letras codificadas en un mensaje. En español, la letra e es la letra más utilizada (aparece el 13,68% de las veces), seguida de la 'a' (el 12,53 %). La letra menos utilizada es la w.

En el menú no necesitaremos más la línea print(mensaje_frec) así que lo comentaremos para que se ignore cuando ejecutemos el código. 

 elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            # print(mensaje_frec)

Crear el gráfico con las frecuencias.

Vamos a definir una nueva función llamada crear_grafico(). Esta función tendrá dos parámetros, texto y lenguaje. El gráfico será un gráfico de barras que tendrá por título "análisis de frecuencias" y las etiquetas para el eje-x serán las claves del diccionario. (a,b,c...y,z)

El diccionario de frecuencias lo pasaremos posteriormente a la función cuando se la llame a través del parámetro texto.

Primeramente después de haberlo instalado importamos al principio del programa el tipo de gráfico que vamos a crear.

from pygal import Bar

#...
def crear_grafico(texto, lenguaje):
    """Crea el gráfico de frecuencias"""
    grafico = Bar(width=800, height=400, title="Análisis de Frecuencias",
                  x_labels=list(lenguaje.keys()))

Etiquetaremos el gráfico usando la frecuencia con la que las letras aparecen en el mensaje codificado y las frecuencias conocidas de las letras en el idioma español. Estos datos los pasaremos a través de los parámetros texto y lenguaje. 

Para pasar la frecuencia con la que las letras aparecen en un texto genérico en el idioma español, crearemos una variable llamada "spanish" y a través de un diccionario pasaremos las frecuencias.

Tenemos que solucionar un problema. A partir de la versión 3.7 de Python los datos en un diccionario se guardan en el orden en el que se han introducido y aunque no fuera así para poder representar correctamente el gráfico tenemos que emparejar las frecuencias de las letras en el mensaje codificado, con las frecuencias de las letras en el idioma. Es decir si la primera letra de nuestro texto codificado es una 'e' y las segunda una "s" pues tendremos que ordenar todas alfabéticamente y hacer coincidir sus valores con los del diccionario del lenguaje, que están ordenados alfabéticamente.

Para ello creamos una lista llamada "valores" que recogerá las frecuencias de las letras del mensaje codificado pero ordenadas alfabéticamente desde la 'a' a la 'z'. A través de un bucle for recorremos las letras "a,b,c...z" y si la letra existe pasamos su valor a la lista y sino pues sustituimos su valor con un cero.

def crear_grafico(texto, lenguaje):
    """Crea el gráfico de frecuencias"""
    grafico = Bar(width=800, height=400, title="Análisis de Frecuencias",
                  x_labels=list(lenguaje.keys()))
    # Etiquetas de las frecuencias para el mensaje codificado.
    valores = []
    for i in lenguaje:
        valores.append(texto.get(i,0))
    grafico.add('Mensaje objetivo', valores)
    # Etiquetas de las frecuencias de las letras en el idioma español
    grafico.add('Lenguaje', list(lenguaje.values()))
    
    grafico.render_to_file('grafico.svg')


# Estructura de datos
# Lista creada desde una cadena de texto
ALFABETO = list(" abcdefghijklmnñopqrstuvwxyz ")
codigo = {}
spanish = {'a': 12.53, 'b': 1.42, 'c': 4.68, 'd': 5.86, 'e': 13.68, 'f': 0.69, 'g': 1.01,
           'h': 0.70, 'i': 6.25, 'j': 0.44, 'k': 0.02, 'l': 4.97, 'm': 3.15, 'n': 6.71,
           'ñ': 0.31, 'o': 8.68, 'p': 2.51, 'q': 0.88, 'r': 6.97, 's': 7.98, 't': 4.63,
           'u': 3.93, 'v': 0.90, 'w': 0.01, 'x': 0.22, 'y': 0.90, 'z': 0.52}

LLamando a la función que representa las frecuencias.

Busca la secuencia elif dentro de la función menu(). Añade una línea de código para utilizar las frecuencias del idioma español y otra para crear el gráfico.

elif eleccion == 'f':
            mensaje = obtener_texto('datos.txt')
            codigo = atbash(mensaje)
            mensaje_frec = frecuencia(codigo)
            # print(mensaje_frec)
            lang_freq = spanish  # Importamos la frecuencia de la letras en español
            # LLama a la función que crea el gráfico
            crear_grafico(mensaje_frec, lang_freq)

Ejecuta el código y puedes ver el archivo del gráfico usando cualquier navegador para abrirlo:

grafico de frecuencias



Analiza el gráfico de frecuencias. 

El gráfico que se ha producido muestra la frecuencia de letras en el idioma español, etiquetado como "Lenguaje". Puedes ver que la letra "e" es la letra más utilizada en el idioma español porque tiene la barra más alta para todos los valores de idioma.

El gráfico de frecuencias también lista la frecuencia de letras en tu mensaje codificado, etiquetado como "Mensaje objetivo". Para averiguar qué codificación se ha utilizado para este mensaje, puedes comparar las barras que muestran el idioma español con las barras en el mensaje codificado. La barra más alta en el texto del mensaje codificado probablemente será una "e". La segunda letra más alta probablemente será una "a", ya que es la siguiente letra más popular.

Los descifradores pueden utilizar la frecuencia de letras para determinar el tipo de codificación que se ha utilizado en el mensaje. Pueden utilizar el método de ensayo y error para predecir qué letra podría representar usando el gráfico como guía.

No obstante ten en cuenta que tu mensaje es muy pequeño lo que dificulta el descifrarlo mediante el análisis de frecuencias. Sin embargo todo se normaliza si codificas un texto lo suficientemente largo.

Puedes encontrar el código de este post en este enlace de Github.

martes, 23 de marzo de 2021

Las listas en Python



una_lista = ['peras', 'manzanas', 'zanahorias']

Las Listas. 


Usamos las listas para guardar múltiples elementos en una sola variable.

Las listas son uno de los cuatro elementos que se usan para almacenar datos, los otros son las Tuplas, los Conjuntos y los Diccionarios.

Las listas se crean usando corchetes.

nombre_lista = [elemento1, elemento2,....., elemento_n]

Los elementos de una lista están ordenados, se pueden modificar y permiten valores duplicados.

Al estar ordenados podemos acceder a cualquier elemento de la lista indicando su posición en la misma. Asi empezando por la izquierda, el primer elemento tiene el indice [0], el segundo [1] y así sucesivamente. Este orden no cambia de forma que si añadimos nuevos elementos a la lista estos se situarán al final de la misma.

Veamos algunos ejemplos de como acceder a una lista usando una lista con números.

>>> lista = [1,2,3,4,5,6,7,8,9,10]
>>> # Si queremos por ejemplo imprimir el numero 4 (cuyo índice es el 3)
>>> print(lista[3])
4

Si quisiéramos imprimir toda la lista podemos usar:

>>> print(lista)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> # o también [:] lo que quiere decir que imprima desde el primer al ultimo elemento de la lista >>> print(lista[:]) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Si quisiéramos imprimir o usar solo una parte o porción de la lista (Slicing o rebanadas en python), usamos la expresión:

lista[desde_posicion, hasta posición sin incluir ese elemento, salto]

>>> # Imprimir los 5 primeros números
>>> print(lista[0:5])
[1, 2, 3, 4, 5]
>>> # Imprimir desde el 6 al final.
>>> print(lista[5:10])
[6, 7, 8, 9]
>>> print(lista[5:])
[6, 7, 8, 9, 10]
>>> # Imprimir cada dos números desde el primer al ultimo elemento
>>> print(lista[::2])
[1, 3, 5, 7, 9]


También podemos indicar el índice del elemento empezando a contar desde la izquierda, en ese caso utilizaremos el índice con el signo menos y empezando por el -1 y no por el cero como hicimos antes. Podemos combinar ambas formas de indicar el índice de los elementos.

>>> # Imprimir los 5 primeros números
>>> print(lista[-10:-5])
[1, 2, 3, 4, 5]
>>> # Imprimir desde el 6 al final.
>>> print(lista[-5:10])
[6, 7, 8, 9]
>>> print(lista[-5:])
[6, 7, 8, 9, 10]
>>> # Imprimir cada dos números desde el último al primero
>>> print(lista[::-2])
[10, 8, 6, 4, 2]
>>> # Invertir el orden de los elementos de una lista.
>>> print(lista[::-1])
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]


Longitud de una lista.


Para saber cuantos elementos tiene una lista usaremos la función len()

Ejemplo

Imprimimos el tamaño o número de elementos de una lista:

>>> lista = ["manzana""platano""pera"]
>>> print(len(lista))
3


Tipos de datos que pueden formar una lista.


Los elementos que se pueden almacenar en una lista pueden ser.....CUALQUIERA. Pueden ser números, strings o cadenas de texto, booleanos o incluso una combinación de todos ellos.

lista_string = ["manzana""platano""pera"]
lista_numeros = [15793]
lista_booleanos = [TrueFalseFalse]
lista_combinada = [True, 5, "manzana"]

También puede haber una lista con otra u otras listas dentro:

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(lista[len(lista)])
4

Como ves la lista tiene 4 elementos, los tres primeros son frutas y el cuarto es otra lista. Si queremos acceder a un elemento de esta última lista es muy sencillo. Primero indicamos su índice en la lista principal y luego otra vez el elemento al que queremos acceder dentro de la sub-lista de números. Por ejemplo para imprimir el nº 3 de la lista de números:

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(lista[3][2])
3
# [Elemento con indice 3 de la lista principal] y [2 de la sublista]


Desde la perspectiva de Python las listas son definidas como objetos cuyo tipo de datos es "list"

>>> lista = ["manzana", "platano", "pera", [1, 2, 3, 4]]
>>> print(type(lista))
<class 'list'>


y también es posible usar el constructor "list()" para crear una lista a partir de otro tipo de objeto:

>>> palabra ="Bienvenido"
>>> print(list(palabra))
['b', 'i', 'e', 'n', 'v', 'e', 'n', 'i', 'd', 'o']


Métodos que podemos usar para trabajar con listas.


*)     nombre_lista.append(elemento)        Añade un elemento al final de la lista.

>>> lista = ['uno','dos','tres']
>>> item = 'cuatro'
>>> lista.append(item)
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']
Otra forma usando slicing de listas es la siguiente:

>>> lista = ['uno','dos','tres']
>>> item = ['cuatro']
>>> lista[len(lista):]=item
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']

Aunque quizá la forma más fácil de fusionar o añadir una lista a continuación de otra es usar las suma "+", vamos sumando ambas listas:

>>> lista = ['uno','dos','tres']
>>> item = ['cuatro']
>>> lista = lista + item
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro']

y ya que estamos con los signos matemáticos si usas el signo de multiplicar con una lista la multiplicas n veces:

>>> lista = ['uno','dos'] * 3
>>> print(lista)
['uno', 'dos', 'uno', 'dos', 'uno', 'dos']


*)     nombre_lista.insert(i, item)                 Añade el item en la posición i desplazando al resto hacia la derecha.

>>> # añadimos el cuatro donde estaba el dos, es decir en la posición 1.
>>> lista = ['uno','dos','tres']
>>> item = 'cuatro'
>>> lista.insert(1,item)
>>> print(lista)
['uno', 'cuatro', 'dos', 'tres']


*)     nombre_lista.extend(otra_lista)            Fusiona o añade una lista a la anterior.

>>> lista = ['uno','dos','tres']
>>> items = ['cuatro','cinco']
>>> lista.extend(items)
>>> print(lista)
['uno', 'dos', 'tres', 'cuatro', 'cinco']

otro ejemplo:

>>> lista = [1,2,3,4]
>>> lista.extend(range(5,11))
>>> print(lista)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


En el ejemplo he utilizado la función range() que lo que hace es crear una lista de números secuenciales.

Su estructura es:

 range(nº inicio, nº final sin incluir, salto)


*)       nombre_lista.index(elemento)                 Nos devuelve el índice o posición del primer elemento de la lista que coincida con el elemento dado.

>>> lista = ["manzana", "platano", "pera", "platano"]
>>> print(lista.index("manzana"))
0
>>> print(lista.index("platano"))
1
>>> print(lista.index("pera"))
2


*)     nombre_lista.pop(<indice_elemento>)       Modifica la lista original, eliminando el elemento de la misma que esté en la posición indicada y lo asigna a una variable si es el caso. El argumento del método es opcional y si no pones nada se entiende que es el último elemento de la lista.

>>> lista_original = ["manzana", "platano", "pera", "platano"]
>>> lista_original.pop() # quita el ultimo elemento de la lista modificándola.
>>> print(lista_original)
['manzana', 'platano', 'pera']
x= lista_original.pop(1) # quitamos el elemento de la posición 1 y lo asignamos a x.
>>> print(x)
platano
>>> print(lista_original) # Como queda la lista después de las modificaciones.
['manzana', 'pera']


Con esto podemos utilizar cosas que se encuentran en otros lenguajes de programación como son las pilas y colas de datos. 

En las pilas se sigue el método LIFO (Last In First Out), es decir, los últimos datos que entran en las listas son los primeros en salir. Utilizaremos los métodos append() y pop(). Veámoslo con este ejemplo. Tenemos un almacén  nos traen la mercancía en cajas. Y la vamos vendiendo primero las últimas que nos traen.

>>> lista_cajas = [5,2,6,7]
>>> # Nos traen otras dos cajas y las metemos en el stock
>>> lista_cajas.append(2)
>>> print(lista_cajas)
[5, 2, 6, 7, 2]
>>> # Ahora sacamos 9 del almacén utilizando el método de la pila. (7, 2)
>>> lista_cajas.pop()
>>> list_cajas.pop()
>>> print(lista_cajas)
[5, 2, 6]


En las colas pasa lo contrario. Va saliendo de la variable primero lo que entro antes. Utilizaremos los métodos insert(0, item) y pop(). Veámoslo con un ejemplo.

>>> lista_cajas = [5,2,6,7]
>>> # Nos traen otras dos cajas y las metemos en el stock
>>> lista_cajas.insert(0, 2)
>>> print(lista_cajas)
[2, 5, 2, 6, 7]
>>> # Ahora sacamos 7 del almacén utilizando el método de la pila. (7)
>>> lista_cajas.pop()
>>> print(lista_cajas)
[2, 5, 2, 6]


*)   nombre_lista.count(item)           Cuenta las veces que un objeto está en una lista.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> print(lista.count(2))
3


*) nombre_lista.remove(item)            Elimina el primer elemento que coincide de una lista.     

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.remove(2)
>>> print(lista)
[5, 2, 6, 7, 2]


Para eliminar un elemento de una lista también podemos usar su posición con la instrucción:

del lista[indice_elemento]

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Borramos el primer 2 de la lista que ocupa la posición 0.
>>> del lista[0]
>>> print(lista)
[5, 2, 6, 7, 2]


 *)  nombre_lista.clear()            Vacía la lista de elementos.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Vaciamos todos los elementos de la lista.
>>> lista.clear()
>>> print(lista)
[]     


Podemos conseguir lo mismo también con las siguientes instrucciones:

>>> lista=[2, 5, 2, 6, 7, 2]
>>> # Vaciamos todos los elementos de la lista.
>>> lista=[]
>>> print(lista)
[]
# O también podemos vaciarla entera o solo una parte usando
# del y usando slicing de listas.
>>> lista=[2, 5, 2, 6, 7, 2]
>>> del lista[:]
>>> print(lista)
[]


*)     nombre_lista.reverse()        Devuelve la misma lista pero al revés.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.reverse()
>>> print(lista)
[2, 7, 6, 2, 5, 2]
# lo mismo lo podríamos conseguir con lista[::-1]


*)     nombre_lista.sort()            Ordena solamente los elementos de una lista. Por defecto modifica la lista orignal y lo hace en orden creciente, a no ser que como argumento usemos reverse=True que lo haría en orden decreciente.

>>> lista=[2, 5, 2, 6, 7, 2]
>>> lista.sort() # Orden creciente
>>> print(lista)
[2, 2, 2, 5, 6, 7]
>>> lista.sort(reverse=True) # Orden decreciente
[7, 6, 5, 2, 2, 2]


Opcionalmente también podríamos usar la función sorted(), que no solo puede ordenar listas sino cualquier otro iterable. Además sorted() no modifica la lista original, sino que devuelve la lista ordenada. 

>>> lista=[2, 5, 2, 6, 7, 2]
>>> x = sorted(lista)
>>> print(x)
[2, 2, 2, 5, 6, 7]
>>> print(lista)
[2, 5, 2, 6, 7, 2]


*)     nombre_lista.copy()         Devuelve una copia de esa lista. Si simplemente asignamos una lista a una variable no estamos copiando la lista en otra independiente sino solo haciendo una referencia a la misma. Por tanto si lo que queremos es copiar una lista tenemos que hacer uso del método .copy()

>>> lista=[2, 5, 2, 6, 7, 2]
>>> x = lista # Esto no copia la lista en una independiente, solo hacemos una referencia
a la misma.
>>> x.append(9)
>>> print(lista)
[2, 5, 2, 6, 7, 2, 9]
>>> z = lista.copy() # Crea una verdadera copia independiente de la lista.
>>> print(z)
[2, 5, 2, 6, 7, 2, 9]


Otras funciones y otras cosas que se pueden aplicar a una lista.


*)     sum(lista)            Devuelve la suma de los elementos de la lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(sum(lista)) 
33


*)     max(lista)            Devuelve el valor máximo de una lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(max(lista)) 
9


*)    min(lista)            Devuelve el valor mínimo de una lista.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> print(min(lista)) 
2


Cuando estamos asignando elementos de un iterable a unas variables, si a una de estas variables le anteponemos un *, Python se encarga de asignarle todos los valores del iterable que le dejen la asignación de las otras variables.

>>> lista = [2, 5, 2, 6, 7, 2, 9]
>>> a, *b, c = lista
>>> print(a)
2
>>> print(b)
[5, 2, 6, 7, 2]
>>> print(c)
9


*) Operador especial "in" aplicado a una lista.         Nos dice si un determinado elemento esta en la lista.

Devuelve True si el elemento esta en la lista y False en caso contrario.

>>> lista = ['angel','andres','maria']
>>> print('maria' in lista)
True


Compresión de listas (list comprehensions).

Nos sirve para crear una nueva lista, excluyendo o trasformando sus elementos, pero de una forma más rápida y que se lea mejor que utilizando el código normal y en una sola línea de código.  

Toda compresión de listas puede ser representada con un bucle for, pero no todo bucle for, puede ser representado como una compresión de listas.

La síntesis de una compresión de listas es la siguiente:

[expression for item in lista condicional]

Esto se puede usar con cualquier iterable, no solo con las listas.

Lo mejor es verlo con ejemplos:

Vamos a obtener una nueva lista, de una lista que ya tenemos, con los números que sean mayores que 10.

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = [x for x in lista if x>10]
>>> print(lista_nueva)
[22, 58, 20]

Esto mismo se podría haber hecho de forma, digamos tradicional usando el siguiente código:

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = []
>>> for x in lista:
>>>   if x > 10:
>>>    lista_nueva.append(x)
print(lista_nueva)
[22, 58, 20]


Otro ejemplo. Multiplicar por 2 los elementos de una lista.

>>> lista = [4,22,58,4,1,20,8]
>>> lista_nueva = [x * 2 for x in lista]
>>> print(lista_nueva)
[8, 44, 116, 8, 2, 40, 16]

 

O por ejemplo también podemos usar  bucles anidados para ciertas cosas, por ejemplo crear puntos de coordenadas:

>>> points = [(x, y) for y in range(0, 3) for x in range(0, 5)]
>>> print(points)
[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), 
 (0, 1), (1, 1), (2, 1), (3, 1), (4, 1), 
 (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
que de forma tradicional llevarían más trabajo:

  1. points = []
  2. for y in range(0, 3):
  3. for x in range(0, 5):
  4. points.append((x, y))
  5. print(points)

e incluso podríamos haber utilizado una condición para mostrar solo los puntos en los que x == y.

  1. points = [(x, y) for x in range(0, 5) for y in range(0, 5)
  2. if x == y]
Para finalizar, si alguna vez necesitamos evaluar una lista, es útil usar los comandos ALL y ANY. (todos o alguno en español)

Por ejemplo si tenemos una serie de números en una lista y queremos saber si son mayores que uno dado podemos usar lo siguiente para abreviar código.

> numeros = [55, 44, 33, 22, 7]
> # ¿Son todos mayores que 5?
> if all([i>5 for i in numeros]):
>	print("Todos los números son mayores que 5.")
Todos los números son mayores que 5.

O si tenemos una lista de números y queremos saber si alguno de ellos es par podemos usar:

> numeros = [55, 44, 33, 22, 7]
> # ¿Alguno de ellos es par?
> if any([i%2==0 for i in numeros]):
     print("al menos un número es par")
> else:
     print("ninguno es par")    

Puedes encontrar más información sobre listas, tuplas y secuencias en la documentación oficial.