Programación modular con Python

Programación modular con Python

Programación modular con Python - 100 días de Python #17

En esta ocasión, dejamos un poco Tkinter para entrar en el paradigma de la programación modular con Python. Un tema fundamental para poder crear programas grandes y muy complejos con un buen rendimiento.

¿Qué es la programación modular?

La programación modular es una forma o técnica de programar que consiste sencillamente en realizar divisiones de un programa grande en diversos módulos o subprogramas.

¿Qué aporta la programación modular?

La programación modular aporta un mayor manejo del código y lo hace más fácil de leer. ¿Te imaginas todo el código de Python y sus módulos integrados en un solo archivo? Traería muchos problemas y sería poco eficiente. Por no decir del tema del espacio de nombres o namespace del cual hablaré en este capítulo.

Los módulos de un programa complejo, podrán ser probados de manera individual. Si ocurre un fallo en uno de ellos, solo tendremos que solucionarlo y no tiene por qué dejar de funcionar todo el programa, solo esa parte.

Prácticamente, todo lo de tecnología está hecho con módulos. Un smartphone, cuenta con un módulo de carga, un módulo de cámara, un módulo de huella, etc.

Un buen ejemplo de esto son los Smartphones Xiaomi, aunque hay otros que también funcionan así. En un smartphone separado por módulos, si se estropea el módulo de carga, lo reemplazamos por otro (normalmente no se repara por el bajo coste de este).

¿Qué ocurre con los smartphones que llevan este módulo soldado en la placa base? Qué los tenemos que reparar, hay que desoldar la parte afectada y soldar una nueva, ya que cambiar la placa base no saldría a cuenta.

En Python igual. Supón que hacemos un videojuego y vemos que los vehículos están teniendo problemas. Si tenemos todo en un mismo sitio, nos costará mucho encontrar el problema entre miles de líneas de código. En cambio, si tenemos un módulo para los vehículos, iremos a revisar el fallo en dicho módulo. Más fácil, ¿verdad?

Dependencia entre módulos - Programación modular

La idea de esta técnica de programación, es crear programas con poca o ninguna dependencia entre sus módulos. Por lo tanto, siempre que sea posible, intenta crear módulos que, si fallan, no afecten al resto.

¿Cómo se crea un módulo Python?

A todo esto, ¿cómo se crea un módulo en Python?

Crear un módulo en Python es tan fácil como hacer un archivo nuevo. No hay que establecer una sintaxis especial ni nada.

Módulo Tkinter de Python - Programación modular

Abramos el módulo de Tkinter en el explorador del sistema. En mi caso la ruta es esta:

D:\Users\PF\AppData\Local\Programs\Python\Python310\Lib\tkinter

Cambia "PF" por tu nombre de usuario y si no tienes la versión 3.10+ de Python, puede que tengas que cambiar esa parte de "python310".

Lo que encontramos aquí, son módulos de Tkinter. Sí, el "módulo" Tkinter, en realidad está dividido en varios módulos más con el fin de no poner todo el código junto. Ya que solo el archivo __init__.py de Tkinter (el que hemos usado hasta ahora) tiene más de 4500 líneas de código.

Programación modular con Python

Crear un módulo en Python - Programación modular

Vamos a crear un nuevo módulo en Python, algo muy sencillo. Una función que calcula potencias.

def pow(numero1, numero2):
    return numero1 ** numero2

calculo1 = pow(2,7)

print(calculo1)

Resultado en la consola

128

Copia y pega este código en un archivo .py. Luego, haz click derecho sobre el nombre de la función y "Mostrar jerarquía de llamadas".

Lo que nos aparece, es que esta función, pertenece al módulo d017.py (nombre de mi archivo).

crear un módulo en python

Los namespace o espacios de nombres y el alcance - Programación modular

¿Qué es el alcance o scope en programación?

El alcance, conocido en inglés como scope, es la zona del código en la cual está accesible un elemento. Lo entenderás mejor con un ejemplo.

¿Qué ocurre aquí?

nombre = "Programación Fácil"

def imprimir_nombre():
    nombre = "PCMaster"
    print(f"El nombre es {nombre}")

imprimir_nombre()

print(nombre)

Resultado en la consola

El nombre es PCMaster
Programación Fácil

¿Por qué "nombre" no se reasigna? ¿Tenemos dos variables llamadas "nombre"?

El código de las funciones tiene un alcance local, lo que significa que todas las variables que creamos o reasignamos dentro de estas, solo son alcanzables desde dentro, pero no desde fuera. Por eso se utiliza "return" en ellas, para devolver datos alcanzables dentro del propio programa.

Las variables que creemos fuera de cualquiera de estos elementos de alcance local, van a ser globales, es decir, se pueden acceder hasta incluso dentro de las funciones. "nombre = "Programación Fácil"" es una variable global. "nombre = "PCMaster"" es una variable local.

Mira lo que pasa si quito la variable local de la función:

nombre = "Programación Fácil"

def imprimir_nombre():
    print(f"El nombre es {nombre}")

imprimir_nombre()

Resultado en la consola

El nombre es Programación Fácil

Como conclusión, puesto que la variable es global, se puede acceder dentro de la función. En cambio, si es a la inversa, no.

¿Qué son los namespaces o espacios de nombres?

Ahora que ya comprendes los alcances, entenderás mejor lo que es un espacio de nombres.

Los espacios de nombres, podríamos decir, que son las fronteras del código o su jurisdicción. Volviendo al ejemplo de antes:

nombre = "Programación Fácil"

def imprimir_nombre():
    nombre = "PCMaster"
    print(f"El nombre es {nombre}")

imprimir_nombre()

print(nombre)

La declaración de la primera variable "nombre" está en el espacio de nombres que ocupa todo el archivo.py. La segunda declaración de "nombre" se encuentra en otro espacio de nombres, el que tiene la función. Por lo tanto, al ser dos espacios de nombres, estas son dos variables diferentes.

¿Por qué se llama espacio de nombres?

El espacio de nombres, se llama así, porque afecta a cualquier cosa que lleve nombre, ya sean variables, funciones, clases, etc.

El alcance de bloque en Python

El alcance de bloque no existe en Python. Este tipo de alcance es el que se tiene en algunos lenguajes de programación, el cual afecta a los bucles y condicionales. Por lo tanto, quiere decir que en Python sí que podemos alcanzar variables declaradas dentro de condicionales o bucles:

for x in range(5):
    valor = f"El valor del bucle es {x}."

print(x)

Resultado en la consola

4

Piensa que el print(), al estar fuera del bucle, me imprime solo el último valor que recibió la variable "valor". Pero puedes comprobar que la variable, en esta ocasión, sí que es accesible. En otros lenguajes de programación, no se puede llamar a la variable "x" desde fuera del bucle.

Problemas con las importaciones de módulos - Programación modular

Ahora que ya te he explicado un poco sobre los alcances y espacios de nombre, es el momento de que aprecies este warning, que si te has fijado, llevamos teniendo en el curso, si no me equivoco, desde el día 8 que empezamos con Tkinter.

from tkinter import *

Warning en el programa

Wildcard import from a library not allowedPylancereportWildcardImportFromLibrary

Este warning, es para recomendarnos no utilizar esta mala práctica de importar todo de un módulo. ¿Cuál es el problema?

Bien, esto importa todos los nombres (funciones, variables, clases…) de dicho módulo, excepto los que empiezan con guion bajo _ (ya explicaré esto otro día). Tenerlo así, es como tener todo el código de la importación sobre nuestro archivo .py. Es como si estuviera ahí mismo. Esto supone, que en el espacio de nombres de nuestro archivo (en el que hemos realizado la importación), tenemos cargados todos esos nombres. ¿Qué supone esto? Veamos un ejemplo.

Volvamos a la función llamada pow() de antes, para calcular potencias. Aún no he importado ningún módulo.

def pow(numero1, numero2):
    return numero1 ** numero2

calculo1 = pow(2,7)

print(calculo1)

Resultado en la consola

128

Ahora, quiero utilizar funciones matemáticas. Para esto, existe un módulo en Python llamado "math", el cual, dedicaré algún capítulo más adelante.

Importar el módulo Math de Python - Programación modular

Lo siguiente que haré es importar el módulo "math" de Python.

from math import *

def pow(numero1, numero2):
    return numero1 ** numero2

calculo1 = pow(2,7)

print(calculo1)

Colisiones de métodos en Python

"math" tiene una función también llamada pow() ¿Qué pasa si la utilizo?

from math import *

calculo1 = pow(2,7)

def pow(numero1, numero2):
    return numero1 ** numero2

calculo2 = pow(2,7)

print(calculo1)
print(calculo2)

Resultado en la consola

128.0
128

Vemos que devuelve un float en el primer print() y un int en el segundo. Eso es porque en la línea 3, aún no se ha declarado mi función pow() y utiliza la función de "math". En la línea 8, se está usando mi función. Se ha sobreescrito esta función en el espacio de nombres.

La función pow() de math, devuelve un float por como está construido:

def pow(__x: _SupportsFloatOrIndex, __y: _SupportsFloatOrIndex) -> float: ...

En cambio, a mi función, al pasarle dos valores int, está dando un valor int. En la función de math, siempre da un float.

Si hago un import sin * puedo utilizar las dos funciones. Eso sí, tengo que estar llamando al módulo cada vez.

import math

def pow(numero1, numero2):
    return numero1 ** numero2

calculo1 = math.pow(2,7)
calculo2 = pow(2,7)

print(calculo1)
print(calculo2)

Resultado en la consola

128.0
128

Pues como puedes ver, usar * en las importaciones, no está recomendado por poder crear errores al sobreescribir nombres en un mismo espacio. Además, si alguna persona que conoce Python, pero no conoce un módulo en específico (hay un montón), no sabrá bien qué métodos son de un módulo en concreto. Por ejemplo, si hay varios módulos que no conoce una persona en un programa, entre ellos Tkinter. En el código aparecen cosas como "Radiobutton()" y muchas otras cosas de los otros módulos en el código. Esta persona tendrá que ir buscando muchos de esos elementos para saber bien como funciona el programa. Esto puede dificultar mucho la programación. Por no decir ya, si vamos a importar módulos de terceros donde puede haber muchos nombres iguales que los nuestros.

Por lo tanto, ahora que ya puedes apreciar este tema, a partir de ahora, empezaremos a utilizar más los alias. No obstante, si no quieres usarlos para realizar prácticas, lo puedes hacer.

Añadir un alias a la importación de módulos - Programación modular

Recuerda que ya mostré que se podía especificar un alias para abreviar las llamadas al módulo, aquí un ejemplo con Tkinter. Fíjate en la línea 2. No estoy utilizando un import all (*). Ahora, me darán error las siguientes líneas

  • Línea 15 con el objeto Tk().
  • Línea 24 con LabelFrame().
  • Líneas 35, 40, 45 y 50 con las etiquetas Label().
  • Línea 56 con la variable de control.
  • Líneas 60, 66, 72 y 78 con los Radiobutton.
  • Líneas 87 y 92 con las etiquetas Label().
  • Línea 98 con el Button().
#Importaciones
import tkinter as tk
import os
from PIL import ImageTk,Image

# Directorio de imágenes principal
carpeta_principal = os.path.dirname(__file__)
# Directorio de imágenes
carpeta_imagenes = os.path.join(carpeta_principal, "imagenes")
carpeta_logos = os.path.join(carpeta_imagenes, "logos")
carpeta_usuarios = os.path.join(carpeta_imagenes, "usuarios")
carpeta_graficos = os.path.join(carpeta_imagenes, "graficos")

#Creación de la ventana principal
root = Tk()
#Título de la ventana
root.title("American Rider Login")
#Icono de la ventana
root.iconbitmap(os.path.join(carpeta_logos, "logo.ico"))

root.configure(background="gray98")

#Marco - Sección usuarios
marco_usuarios = LabelFrame(root,
                            text="Seleccione un usuario:",
                            padx=10,
                            pady=10,
                            background="gray98",
                            border=0)
marco_usuarios.pack(padx=10, pady=10)

#Carga de imágenes
#Usuario 1
usuario_1 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "emma.png")).resize((200,200)))
etiqueta = Label(marco_usuarios, image=usuario_1, background="gray98")
etiqueta.grid(row=0, column=0)

#Usuario 2
usuario_2 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "noah.png")).resize((200,200)))
etiqueta = Label(marco_usuarios, image=usuario_2, background="gray98")
etiqueta.grid(row=2, column=0)

#Usuario 3
usuario_3 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "jacob.png")).resize((200,200)))
etiqueta = Label(marco_usuarios, image=usuario_3, background="gray98")
etiqueta.grid(row=0, column=1)

#Usuario 4
usuario_4 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "sophia.png")).resize((200,200)))
etiqueta = Label(marco_usuarios, image=usuario_4, background="gray98")
etiqueta.grid(row=2, column=1)

#Boton entrar
boton_entrar = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_graficos, "boton_enviar.png")))

opcion = StringVar()
opcion.set("Error")

#Radiobutton
Radiobutton(marco_usuarios,
           text="Emma", 
           variable=opcion,
           value="Emma",
           background="MediumPurple3").grid(row=1, column=0)

Radiobutton(marco_usuarios,
           text="Noah",
           variable=opcion,
           value="Noah",
           background="DarkSlateGray4").grid(row=3, column=0)

Radiobutton(marco_usuarios,
           text="Jacob",
           variable=opcion,
           value="Jacob",
           background="olive drab").grid(row=1, column=1)

Radiobutton(marco_usuarios,
           text="Sophia",
           variable=opcion,
           value="Sophia",
           background="salmon").grid(row=3, column=1)

# Función para el botón de envío
def actualiza_radio():
    if opcion.get() == "Error":
        Label(root, 
        text=f"¡No has seleccionado ninguna cuenta! Por favor, inténtelo de nuevo",
        background="gray98",
        foreground="red2").pack()
    else:
         Label(root, 
         text=f"Hola {opcion.get()}. Accediendo a tu cuenta personal...",
         background="gray98"
         ).pack()

# Botón de envío
boton_envia = Button(root,
           text="Entrar",
           command=actualiza_radio,
           image=boton_entrar,
           border=0, 
           background="gray98"
           ).pack(pady=10)

#Bucle de ejecución
root.mainloop()

Como puedes ver, falla todo lo que es propio de la importación del módulo de Tkinter, ya que ahora, ya no está ocupando el mismo espacio de nombres.

Depurar un programa de Python

¿Qué es depurar en programación?

La acción de depurar en programación, es el proceso de identificación y corrección de errores en el código.

¿Quieres ejecutar un programa y que la mayoría de veces lo haga a la primera?

Esto es mucho más fácil de hacer si utilizamos la pestaña de información que trae Visual Studio Code, la cual, nos muestra, antes de ejecutar el programa, todos los errores y "warnings" (alertas). Así es muy fácil realizar la depuración del código.

Depuremos el programa con tantos errores. Vamos a la pestaña inferior izquierda:

errores y warnings en VSCode

Haz click en ella:

Programación modular con Python

Aparecen múltiples errores (14). 0 warnings y 28 informaciones. Estas informaciones te aparecen si tienes la extensión Code Spell Checker para los errores ortográficos. Yo la tengo, porque programo mucho para web y escribo mucho texto. Si no es tu caso, quizás te estorbe un poco. Si quieres más información sobre esto, déjame un comentario.

Gracias a la extensión Pylance, nos sale el tipo de error y la línea donde está, además de la columna (caracter).

Para solucionar los problemas de importación, hay que añadir a todos los errores el prefijo del alias o "tkinter" si no usas alias.

No importa que el nombre del alias sea "tk", puedes poner lo que quieras. Sin embargo, debería representar bien el módulo al que pertenece. Por lo tanto, para Tkinter, no deberías poner, por ejemplo, "modulo_gui", ya que podemos estar utilizando más de un módulo para interfaces gráficas y puede confundir.

Esto quedaría reescrito así (fíjate en las líneas que daban error):

#Importaciones
import tkinter as tk
import os
from PIL import ImageTk,Image

# Directorio de imágenes principal
carpeta_principal = os.path.dirname(__file__)
# Directorio de imágenes
carpeta_imagenes = os.path.join(carpeta_principal, "imagenes")
carpeta_logos = os.path.join(carpeta_imagenes, "logos")
carpeta_usuarios = os.path.join(carpeta_imagenes, "usuarios")
carpeta_graficos = os.path.join(carpeta_imagenes, "graficos")

#Creación de la ventana principal
root = tk.Tk()
#Título de la ventana
root.title("American Rider Login")
#Icono de la ventana
root.iconbitmap(os.path.join(carpeta_logos, "logo.ico"))

root.configure(background="gray98")

#Marco - Sección usuarios
marco_usuarios = tk.LabelFrame(root,
                            text="Seleccione un usuario:",
                            padx=10,
                            pady=10,
                            background="gray98",
                            border=0)
marco_usuarios.pack(padx=10, pady=10)

#Carga de imágenes
#Usuario 1
usuario_1 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "emma.png")).resize((200,200)))
etiqueta = tk.Label(marco_usuarios, image=usuario_1, background="gray98")
etiqueta.grid(row=0, column=0)

#Usuario 2
usuario_2 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "noah.png")).resize((200,200)))
etiqueta = tk.Label(marco_usuarios, image=usuario_2, background="gray98")
etiqueta.grid(row=2, column=0)

#Usuario 3
usuario_3 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "jacob.png")).resize((200,200)))
etiqueta = tk.Label(marco_usuarios, image=usuario_3, background="gray98")
etiqueta.grid(row=0, column=1)

#Usuario 4
usuario_4 = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_usuarios, "sophia.png")).resize((200,200)))
etiqueta = tk.Label(marco_usuarios, image=usuario_4, background="gray98")
etiqueta.grid(row=2, column=1)

#Boton entrar
boton_entrar = ImageTk.PhotoImage(Image.open(os.path.join(carpeta_graficos, "boton_enviar.png")))

opcion = tk.StringVar()
opcion.set("Error")

#Radiobutton
tk.Radiobutton(marco_usuarios,
           text="Emma", 
           variable=opcion,
           value="Emma",
           background="MediumPurple3").grid(row=1, column=0)

tk.Radiobutton(marco_usuarios,
           text="Noah",
           variable=opcion,
           value="Noah",
           background="DarkSlateGray4").grid(row=3, column=0)

tk.Radiobutton(marco_usuarios,
           text="Jacob",
           variable=opcion,
           value="Jacob",
           background="olive drab").grid(row=1, column=1)

tk.Radiobutton(marco_usuarios,
           text="Sophia",
           variable=opcion,
           value="Sophia",
           background="salmon").grid(row=3, column=1)

# Función para el botón de envío
def actualiza_radio():
    if opcion.get() == "Error":
        tk.Label(root, 
        text=f"¡No has seleccionado ninguna cuenta! Por favor, inténtelo de nuevo",
        background="gray98",
        foreground="red2").pack()
    else:
         tk.Label(root, 
         text=f"Hola {opcion.get()}. Accediendo a tu cuenta personal...",
         background="gray98"
         ).pack()

# Botón de envío
boton_envia = tk.Button(root,
           text="Entrar",
           command=actualiza_radio,
           image=boton_entrar,
           border=0, 
           background="gray98"
           ).pack(pady=10)

#Bucle de ejecución
root.mainloop()

Importar módulos propios o de terceros en Python - Programación modular

Crear un módulo propio en Python

Lo primero que debes hacer, es crear un módulo. Crea un archivo .py con el nombre que quieras. Yo lo llamaré "suma.py".

Pon una función sencilla, da igual lo que haga, que devuelva algún valor para hacer la prueba.

def sumar(numero1, numero2):
    return numero1 + numero2

Realizar la importación de módulos propios o de terceros

Ahora, desde otro archivo, vas a importar este módulo como si fuese uno más de Python:

import suma

resultado = suma.sumar(10, 50)

print(resultado)

Resultado en la consola

60

A que Python es genial, ¿verdad?

También puedes utilizar el import * o el alias:

from suma import *

resultado = sumar(10, 50)

print(resultado)
import suma as sm

resultado = sm.sumar(10, 50)

print(resultado)

Importar módulos propios en carpetas

¿Se pueden poner estos módulos en carpetas para organizarlo todo mejor?

La respuesta es que sí.

Crea una carpeta dentro de tu proyecto. En la raíz. La mía se llamará "operaciones". Meto el archivo ahí y listo. Ahora, he de añadir con la sintaxis de puntos, tantas carpetas haya hasta llegar al módulo.

Que sepas que también puedes utilizar un alias para no escribir algo tan largo.

import operaciones.suma

resultado = operaciones.suma.sumar(10, 50)

print(resultado)

Dejemos esta introducción a la programación modular con Python y vayamos a practicar un poco con un ejercicio resuelto y un pequeño proyecto con los módulos.

Un comentario en «Programación modular con Python»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

curso Java Entrada anterior Los operadores de Java
curso de Python Entrada siguiente Soluciones de ejercicios de programación modular con Python