He estado estudiando programación orientada a objetos para entenderla mejor y he escrito una introducción a la POO mediante ejemplo practico para que otras personas también puedan beneficiarse.
También iré publicando otros proyectos similares como la vez en la que cree un Blog web offline con soporte PWA
Escritura de un programa simple de salvapantallas con Python, utilizando programación orientada a objetos.
Tabla de Contenidos:
- Introducción
- Sobre el aprendizaje desde los fundamentos
- Estructura del Tutorial
- Requerimientos previos del tutorial
- Desarrollo
- Objeto «universo.cielo.componentes»
- Objeto «universo.estrellas.componentes»
- Raíz del programa: «main.py»
- Flujo del programa
- Inicialización de clases
- Conclusiones
- Webgrafía
Introducción
Como buen principiante que soy, he tenido que deambular buscando tutoriales que me expliquen como hacer ciertas cosas y que resuelven con relativo éxito mis necesidades. Además, mis comienzos en la programación con la universidad fueron muy anticuados, siendo que estudie programación estructurada en lugar de programación orientada a objetos, lo que me dificulta mucho aprender lo ultimo.
Como seguro se habrán dado cuenta, la mayoría de los tutoriales buscan llegar de un punto «A» a un punto «B» sin complicarse mas, para lo que recurren a una forma simple de programación que puede derivar en lo que se conoce como «código spaguetti», mientras que para aprender programación orientada a objetos, hay que recurrir a textos mecánicos que explican superficialmente y usan ejemplos muy cortos que no aportan demasiado para entender los conceptos.
Este tutorial probablemente no sea el mejor de su tipo, pero mi objetivo es escribir algo que siento que me hubiese servido a entender mejor algunos conceptos, para lo cual he recurrido a un programa escrito en Python por Suraj Singh quien ha escrito un tutorial breve, pero ha aportado un código super comentado que he traducido, comentado mas y reestructurado para poder explicar algunos conceptos que considero son necesarios para entender mejor sobre Python y programación orientada a objetos.
Quiero aclarar que el tutorial de Suraj Singh no es malo, de hecho, lo considero genial y es la razón por la que lo estoy usando de ejemplo, sino que quiero expandir lo que enseña para cumplir con mis expectativas sobre el aprendizaje desde los fundamentos.
Sobre el aprendizaje desde los fundamentos
Cuando comenzamos a programar, es muy probable que hayamos empezado por necesidad, por lo cual seguramente habremos buscado con desesperación códigos y los hemos apilado hasta que funcionaran. Esto nos permite aprender muchas cosas y muy rápido, con lo que podemos concluir con éxito lo que nos movió a buscar aprender a programar en primer lugar, pero nos hace confiarnos y descuidar los fundamentos que mas adelante nos permitirían resolver tareas con mas eficiencia y eficacia.
Estructura del Tutorial
Este tutorial tratará de explicar de forma detallada todos los conceptos requeridos para lograr la creación de un salvapantalla simple escrito en Python. Hay que recordar que no es un salvapantalla real, en el sentido de que no puede ser instalado en las plataformas nativas de salvapantallas de Windows o Linux y solo es un modelo básico para explicar los conceptos.
Estará agrupado en dos partes:
- Tutorial documentado con código y gráficos.
- Código comentado, listo para ejecutar.
Requerimientos previos del tutorial
- Saber escribir un hola mundo en Python
Si, solo eso. Hay muchas cosas implicadas en escribir hola mundo en Python, por ejemplo, tener instalado el compilador, saber usar la consola y muchas cosas mas.
Al tener instalado el compilador también se incluyen muchas herramientas y en la mayoría de instalaciones al menos, ya vienen incorporadas las herramientas de Tkinter, para la creación de ventanas en modo gráfico.
Aunque el tutorial puede ser ejecutado en el «Notepad» si les parece bien, yo recomiendo el uso de «Visual Studio Code», junto a los complementos de Python, porque tiene funciones de asistencia que facilita depurar e incluso aprender como funcionan las cosas. Si quieres, puedes presionar la tecla Control + Click izquierdo para poder ver la función, clase o variable donde fueron definidas.
Desarrollo de la introducción a la POO mediante ejemplo practico
La programación orientada a objetos, busca la optimización del código mediante el agrupamiento de funciones y atributos conocidos como «objetos». Para entenderlo provisionalmente, he decidido tratar de visualizar, tanto la estructura de carpetas como la organización de funciones, clases y atributos, como objetos del mundo real, por lo que la estructura del directorio representa una caja y la he etiquetado como «python screensaver»
Dentro de esta caja he creado el primer archivo llamado «main.py» que contendrá la lógica del programa y dentro de la misma, he agregado otra caja que he llamado «universo».
Python permite trabajar con módulos, que no son mas que archivos que contienen código adicional que mantenemos separado del archivo principal para que sea mas fácil de editar y depurar. Durante la ejecución, el interprete unirá todos los archivos en un solo script y los leerá secuencialmente para poder ejecutar el programa en un proceso detallado en el modelo de ejecución, en la documentación oficial.
Para crear un módulo, debemos crear una carpeta que contenga un archivo vacío con nombre «__init__.py». una vez creado, no le volveremos a tocar.
Dado que el programa que estamos escribiendo es un salvapantalla de un cielo estrellado, los nombres de los módulos deberían reflejar esto, por lo que la carpeta universo contendrá al cielo, con sus funciones(o sea, el código que define su comportamiento) y atributos y a las estrellas, también con sus funciones y atributos. (Podríamos ignorar un poco el debate de si el cielo contiene a las estrellas o el universo contiene al cielo). Esto deja nuestro directorio de la siguiente manera:
Es recomendable que cada parte del código sea fácil de entender, que cada nombre y variable sea fácil de deducir y todo este muy bien ordenado porque, aunque al inicio funcione, si hay que modificar o corregir mas adelante, puede llegar a ser muy complicado y enigmático. Es por eso que en esta ocasión he elegido este esquema para los nombres de carpetas y módulos. Por supuesto, si se desea se puede usar cualquier esquema, no hay reglas estrictas en este sentido.
Nota: Usualmente el primer archivo de tu código debería llamarse «main.py», puesto que este es el nombre mas frecuentemente usado para indicar que todo el programa comienza por allí, pero en otros lenguajes de programación, como JavaScript(con NodeJS), se llaman «index.js». Esto no es estrictamente necesario puesto que el primer archivo puede tomar cualquier nombre. Solo es un acuerdo tácito entre programadores para facilitar la lectura del código.
En el archivo «main.py» vamos a comenzar haciendo nuestras primeras importaciones de módulos del sistema:
import tkinter as tk
import time
import random
Los módulos se pueden importar por su nombre, pero durante la escritura del código, tal vez prefieras utilizar un «alias» o apodo, para reducir la cantidad de texto al escribir. En mi caso he importado el módulo tkinter como tk, pero en otros tutoriales pueden optar por otros nombres. Por supuesto, también puedes importarlo sin ningún alias, no hay problema con eso y como ves, la mayor parte de los módulos aquí usados aparte de tkinter, se usan de esa manera.
Nuestro salvapantalla tiene que preparar algunas cosas antes de visualizarse, por lo que nuestro siguiente componente en ser cargado es la clase «cielo» dentro del módulo «universo».
Cuando creamos nuestra estructura de archivos, consideramos la carpeta «Python screensaver» como nuestro contenedor y dentro del mismo, creamos la carpeta «universo».
Al definir el archivo «__init__.py», la carpeta pasa a ser parte del módulo, por lo que al importar el objeto «cielo», la buscará en el módulo universo, según este esquema:
import nombre_módulo.nombre_objeto
Así que importaríamos nuestro módulo de la siguiente manera:
import universo.cielo
Nuestro trabajo ahora consiste en desarrollar el objeto «cielo.componentes».
En el módulo cielo debemos repetir la importación de los módulos, pero en este caso solo hace falta uno:
import tkinter as tk
Además, importaremos también el otro objeto creado en el módulo universo llamado estrellas.
import universo.estrellas
Objeto «universo.cielo.componentes»
Según la Wikipedia:
Un objeto es un ente orientado a objetos (programa de computadoras) que consta de un estado y de un comportamiento, que a su vez constan respectivamente de datos almacenados y de tareas realizables durante el tiempo de ejecución. Un objeto puede ser creado instanciando una clase, como ocurre en la programación orientada a objetos, o mediante escritura directa de código y la replicación de otros objetos, como ocurre en la programación basada en prototipos.
En nuestro caso, los atributos de nuestro módulo «cielo» se encuentran en nuestra clase «componentes» donde está definida una función que establece los parámetros iniciales de funcionamiento y variables que contienen sus atributos (def __init__(self, *args, **kwargs)) y dos funciones que definen el comportamiento del cielo (def crear_estrellas(self) y def actualizar_pantalla(self))
El código de la clase «universo.cielo.componentes» queda entonces de la siguiente manera:
import tkinter as tk
import universo.estrellas
CANTIDAD_ESTRELLAS = 2
class componentes(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.estrellas = []
self.crear_estrellas()
def crear_estrellas(self):
for i in range(CANTIDAD_ESTRELLAS):
self.estrellas.append(universo.estrellas.componentes(self))
return
def actualizar_pantalla(self):
for i in self.estrellas:
i.actualizar()
return
Objeto «universo.estrellas.componentes»
Nota: No hay una forma lineal de escribir código en cuanto a programación orientada a objetos se refiere. Como te has dado cuenta, hasta el momento hemos creado una clase y ahora crearemos otra clase, ninguna de las cuales puede ser ejecutadas individualmente. Nuestro archivo «main.py» continua vacío excepto algunas importaciones que agregamos al principio. Esto es debido a que el código ya ha pasado por algunas pruebas y esquematizaciones, lo que permite crear este código ya organizado.
Es cuestión del programador el determinar la mejor forma de escribir su código, pero para practicar te recomiendo escribir programas sencillos en un solo archivo y practicar luego modularizándolos y convirtiendo en objetos algunas funciones y variables. Lo importante es entender los conceptos antes de tirarse a un proyecto mas grande, aunque si han llegado a este tutorial, es probable que ya hayan puesto en marcha algo de código antes de entender como funciona.
Al igual que en el módulo anterior, hacemos las respectivas importaciones, creamos las variables globales y creamos la clase:
import random
VELOCIDAD_RADIO=[i/10.0 for i in range(-10,-2)]+[i/10.0 for i in range(2,10)]
RADIO=25
COLOR_ESTRELLA = "yellow"
class componentes:
def __init__(self, padre):
self.padre = padre
self.comenzar_movimiento()
self.crear_circulo_pequeño()
def comenzar_movimiento(self):
self.x1 = self.padre.winfo_width()/2
self.y1 = self.padre.winfo_height()/2
self.velocity_x = random.choice(VELOCIDAD_RADIO)
self.velocity_y = random.choice(VELOCIDAD_RADIO)
return
def crear_circulo_pequeño(self):
x1=self.x1
y1=self.y1
x2,y2=x1+RADIO, y1+RADIO
self.estrella = self.padre.create_oval(x1,y1,x2,y2, fill=COLOR_ESTRELLA)
return
def parar_movimiento(self):
self.padre.coords(self.estrella, self.x1,self.y1,self.x1+RADIO,self.y1+RADIO)
self.comenzar_movimiento()
return
def actualizar(self):
self.padre.move(self.estrella, self.velocity_x, self.velocity_y)
x,y = self.padre.coords(self.estrella)[:2]
if x<0 or x>1500:
self.parar_movimiento()
elif y<0 or y>1000:
self.parar_movimiento()
return
Raíz del programa: «main.py»
Podría parecer un poco extraño no haber comenzado desde el inicio, pero el objetivo de este tutorial no es explicar el diseño de un código modular, sino explicar los conceptos implicados en el desarrollo del código mediante programación orientada a objetos. Debido a esto, recién llegamos a la parte que unificará todo el código escrito hasta el momento.
El código entonces, quedaría así:
import tkinter as tk
import time
import random
import universo.cielo
COLOR_FONDO="black"
NIVEL_TRANSPARENCIA=1
EVENTOS_DE_CIERRE = ['<Any-KeyPress>', '<Any-Button>']
def main():
raiz=tk.Tk()#establecer la ventana raiz o inicial
pantalla=universo.cielo.componentes(raiz,bg=COLOR_FONDO)
pantalla.pack(expand="yes",fill="both")
raiz.wait_visibility(pantalla)
raiz.wm_attributes('-alpha',NIVEL_TRANSPARENCIA)
raiz.wm_attributes("-topmost", True)
raiz.overrideredirect(1)
raiz.state('zoomed')
def salir(event):
raiz.destroy()
return
for seq in EVENTOS_DE_CIERRE:
raiz.bind_all(seq, salir)
while True:
raiz.update()
raiz.update_idletasks()
pantalla.actualizar_pantalla()
if __name__ == '__main__':
main()
Flujo del programa
El código ya se encuentra completamente comentado y explicando que hace cada función, variable, clase y modulo, pero hare unas cuantas aclaraciones aquí.
Inicialización de clases
Importar los módulos no es suficiente para empezar a usarlos. Es necesario inicializarlos, para lo cual, se los llama solos o dentro de una variable, pasándoles los parámetros necesarios para funcionar.
El modulo «universo.cielo.componentes» es inicializada en la función «main()» del archivo main.py de la siguiente manera:
pantalla=universo.cielo.componentes(raiz,bg=COLOR_FONDO)
La clase «universo.cielo.componentes» recibe como parámetro, la inicialización que hemos realizado del componente «tkinter». Esto nos evita tener que inicializar las clases y redefinir nuestros parámetros cada vez que lo necesitemos. Reutilizar código es una de las piezas clave de la Programación Orientada a Objetos y en este caso, lo que haremos se conoce como «Herencia»
La clase «universo.cielo.componentes» recibe los parámetros e inicializa las variables y funciones necesarias. Es decir, crea una instancia de la ventana que hemos creado, crea un arreglo que contendrá las estrellas e inicializara la función «crear_estrellas()» para definir el comportamiento inicial de la clase.
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs)
self.estrellas = []
self.crear_estrellas()
En Python, se utiliza la palabra «self» para representar una instancia de la clase. Esto le permite acceder a los atributos y funciones que esta posee. Todas las funciones definidas en la clase, deberán utilizar esta palabra para poder acceder a sus variables inicializadas en «__init__»
El arreglo «self.estrellas» no contendrá datos, sino instancias de la siguiente clase que vamos a inicializar: «universo.estrellas.componentes»
La función «crear_estrellas()» crea un ciclo donde se agregan diferentes instancias de la clase «universo.estrellas.componentes» de acuerdo a la cantidad que hayamos definido en la variable «CANTIDAD_ESTRELLAS»
def crear_estrellas(self):
for i in range(CANTIDAD_ESTRELLAS):
self.estrellas.append(universo.estrellas.componentes(self))
return
Las clases son instanciadas cada vez que son invocadas. En el caso de «universo.estrellas.componentes», sera instanciada 2 veces según lo definido en «CANTIDAD_ESTRELLAS», lo cual significa que el objeto estrella llega a existir dos veces, lo cual es equivalente a tener físicamente dos estrellas, con sus respectivos atributos y comportamiento.
La clase «universo.estrellas.componentes», al momento de ser inicializada, heredará todos los atributos de la clase que la llama, estableciéndose una relación «Padre-Hija» Esto se logra enviando como parámetro la palabra «self» y recibiéndola en una variable que en este caso llamaremos «self.padre»
def __init__(self, padre):
self.padre = padre
self.comenzar_movimiento()
self.crear_circulo_pequeño()
La clase «universo.estrellas.componentes» al ser inicializada, guarda la información de la clase padre en la variable «self.padre» y llama dos funciones: «self.comenzar_movimiento()» y «self.crear_circulo_pequeño()». Estas funciones establecen los parámetros iniciales del objeto en la pantalla y crean el objeto en la pantalla.
Una vez inicializadas las clases, la función «main()» tiene acceso completo a sus atributos y funciones, por lo cual, se procede a actualizarlas desde el loop«While True»
while True:
raiz.update()
raiz.update_idletasks()
pantalla.actualizar_pantalla()
La variable «pantalla» ahora tiene acceso a la función «actualizar_pantalla()» dentro de la clase «universo.cielo.componentes» la cual hará un barrido de los objetos creados en «self.estrellas» para que actualicen sus propiedades.
«universo.cielo.componentes.actualizar_pantalla()»
def actualizar_pantalla(self):
for i in self.estrellas:
i.actualizar()
return
«universo.estrellas.componentes.actualizar()»
def actualizar(self):
self.padre.move(self.estrella, self.velocity_x, self.velocity_y)
x,y = self.padre.coords(self.estrella)[:2]
if x<0 or x>1500:
self.parar_movimiento()
elif y<0 or y>1000:
self.parar_movimiento()
return
Conclusiones
La programación orientada a objetos puede ser muy confusa a veces. Como hemos visto, un programa relativamente sencillo puede ser un laberinto difícil de rastrear, sin embargo, nos permite realizar cosas complejas de forma mas eficiente.
Entender la programación orientada a objetos desde su enfoque inicial, como una representación de los objetos de la vida real, puede facilitar mucho las cosas. Es de sentido común que todo objeto que nos rodea, tiene atributos y comportamientos definidos, pero es un poco mas abstracto los conceptos de herencia, abstracción, polimorfismo, acoplamiento y encapsulamiento, pero es un buen punto de partida.
Conforme se siga practicando, es posible entender cosas mas complejas y también es posible llegar a entender, por que se hizo tan popular en la actualidad.
Webgrafía
Módulos en Python. Por Learn Python. https://www.learnpython.org/es/Modules%20and%20Packages
how to create beautiful moving stars/snow like screen saver using python and tkinter – tutorial – two | python gui example. Por Suraj Singh. https://www.bitforestinfo.com/blog/04/11/how-to-create-beautiful-movingstars-r-snow-like-screensaver-using-python-and-tkinter.html
Objeto(programación) https://es.wikipedia.org/wiki/Objeto_(programación)
Código Espagueti https://es.wikipedia.org/wiki/Código_espagueti
Modelo de Ejecución https://docs.python.org/es/3/reference/executionmodel.html
Herencia (informática)https://es.wikipedia.org/wiki/Herencia_(inform%C3%A1tica)
Código y esquema de directorio:
# Importar los componentes necesarios
import tkinter as tk
import time
import random
#importar los componentes que contienen las clases que hemos creado
import universo.cielo
#Definir variables globales
COLOR_FONDO="black" #Establecer la variable que define el atributo del color del cielo
NIVEL_TRANSPARENCIA=1
EVENTOS_DE_CIERRE = ['<Any-KeyPress>', '<Any-Button>'] #Se puede crear un arreglo de eventos para inicializarlos todos de una vez
def main():
raiz=tk.Tk()#Establecer la ventana raiz o inicial
pantalla=universo.cielo.componentes(raiz,bg=COLOR_FONDO) #crear la ventana, utilizando la clase «cielo» como lienzo donde se dibujaran los componentes
pantalla.pack(expand="yes",fill="both") #agrega los componentes en el lienzo
raiz.wait_visibility(pantalla) #Se espera a que la ventana sea construida para cargar los componentes
raiz.wm_attributes('-alpha',NIVEL_TRANSPARENCIA) #Se agrega el atributo de transparencia a la ventana
raiz.wm_attributes("-topmost", True) #Se agrega el atributo «por encima de todo» a la ventana
raiz.overrideredirect(1) #Evita redimensionar la ventana
raiz.state('zoomed') #Inicia la ventana en estado "maximizado"
#Esta funcion permite terminar el programa al ser disparada por los eventos definidos en la variable EVENTOS_DE_CIERRE
def salir(event):
raiz.destroy()
return
#Permite vincular la ventana a la lista de eventos definidos en la variable «EVENTOS_DE_CIERRE»
for seq in EVENTOS_DE_CIERRE:
raiz.bind_all(seq, salir)
while True:
raiz.update() #Actualiza la ventana
raiz.update_idletasks() #Actualiza la ventana si no hay actividad
pantalla.actualizar_pantalla() #Recarga la ventana mediante la clase cielo
#establecemos la funcion main como arranque al iniciar el script
if __name__ == '__main__':
main()
#Importar los componentes necesarios
import tkinter as tk
#Importar los componentes que contienen las clases que hemos creado
import universo.estrellas
#Crear una variable globlal para la cantidad de estrellas en pantalla.
CANTIDAD_ESTRELLAS = 2
# Definir la clase componentes
class componentes(tk.Canvas):
# Definir la clase inicial, que recibira los parametros desde donde sea inicializada.
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs) # Crear un objeto tkinter con los parametros iniciales
self.estrellas = [] # Establecer un arreglo de «estrellas» obtenidas de la clase «universo.estrellas.componentes»
self.crear_estrellas() # Llamar a funcion crear_estrellas() al inicializar la clase
# Funcion para crear estrellas
def crear_estrellas(self):
for i in range(CANTIDAD_ESTRELLAS): # Crea un arreglo de instancias de universo.estrellas.componentes de acuerdo a la cantidad definida en la variable CANTIDAD_ESTRELLAS
self.estrellas.append(universo.estrellas.componentes(self))
return
#Funcion para actualizar la posicion de las estrellas en el cielo
def actualizar_pantalla(self):
for i in self.estrellas:
i.actualizar()
return
import random
VELOCIDAD_RADIO=[i/10.0 for i in range(-10,-2)]+[i/10.0 for i in range(2,10)]
RADIO=25
COLOR_ESTRELLA = "yellow"
class componentes:
# Definir la clase inicial, que recibira los parametros desde donde sea inicializada.
def __init__(self, padre):
self.padre = padre
self.comenzar_movimiento()
self.crear_circulo_pequeño()
# Establece los parametros iniciales para realizar los movimientos del objeto en la pantalla
def comenzar_movimiento(self):
self.x1 = self.padre.winfo_width()/2
self.y1 = self.padre.winfo_height()/2
self.velocity_x = random.choice(VELOCIDAD_RADIO)
self.velocity_y = random.choice(VELOCIDAD_RADIO)
return
# Crea un circulo en la pantalla
def crear_circulo_pequeño(self):
x1=self.x1
y1=self.y1
x2,y2=x1+RADIO, y1+RADIO
self.estrella = self.padre.create_oval(x1,y1,x2,y2, fill=COLOR_ESTRELLA)
return
# Detiene el movimiento del objeto en pantalla
def parar_movimiento(self):
self.padre.coords(self.estrella, self.x1,self.y1,self.x1+RADIO,self.y1+RADIO)
self.comenzar_movimiento()
return
# Actualiza la posicion del objeto en pantalla
def actualizar(self):
self.padre.move(self.estrella, self.velocity_x, self.velocity_y)
x,y = self.padre.coords(self.estrella)[:2]
if x<0 or x>1500:
self.parar_movimiento()
elif y<0 or y>1000:
self.parar_movimiento()
return
2 respuestas a “Tutorial: Introducción a la POO mediante ejemplo practico ”
[…] que he ido recopilando mientras voy aprendiendo otras cosas. al igual que el tutorial para la introducción a la programación orientada a objetos, el objetivo primordial de escribir esto es guardar un registro que pueda consultar en caso de […]
[…] Este proyecto es parte de otros que he escrito, como la vez en la que escribi un protector de pantalla en python para entender la programacion orientada a objetos. […]