sábado, 27 de abril de 2024

Campeones de Gráficos.

 Basado en el proyecto de la RapsberryPi Org "Charting Champions".

¿Qué es lo que haremos?

Descubriremos el poder de las listas en Python creando un gráfico interactivo de la medallas olímpicas conseguidas por los diferentes países.

Los juegos olímpicos empezaron en el año 1896: miles de atletas representaron a centenares de naciones de todo el mundo. Los juegos olímpicos modernos se inspiraron en las olimpiadas que se realizaban en Grecia en la antigüedad.

Lo que haremos:

- Usar listas para almacenar los datos.

- Crear un gráfico usando la librería Pygal.

- Cargar los datos haciendo que tu programa los lea desde un archivo.


Creando un gráfico.

Lo primero que haremos será instalar la librería que vamos a utilizar para crear el gráfico, la librería Pygal. Para presentar los gráficos por defecto se utiliza el formato SVG (escalable vector graphics), que incluso nos servirá para incluirlo en una página web.

Información: INSTALANDO PYGAL

En WINDOWS:

En la consola de comandos teclea lo siguiente y pulsa la tecla Enter.

pip install pygal

Espera que la instalación se complete y luego continua con el proyecto.

En un MAC:

En una ventana de terminal teclea lo siguiente y pulsa la tecla Enter.

pip3 install pygal

Espera que la instalación se complete y luego continua con el proyecto.

En LINUX, incluyendo el sistema operativo de la Rapsberry Pi.

En una ventana de terminal teclea lo siguiente y pulsa la tecla Enter.

pip3 install pygal

Espera que la instalación se complete y luego continua con el proyecto.
Para comenzar el proyecto importaremos algo de código de la librería pygal que utilizaremos para dibujar el gráfico. Empieza creando un nuevo archivo, por ejemplo main.py, y teclea el siguiente código:

from pygal import Bar

Después vamos a crear un gráfico de barras para mostrar los resultados. Añade el siguiente código y luego ejecútalo:

from pygal import Bar
import webbrowser

# Creando un gráfico
grafico = Bar(title="Medallas Olímpicas")
grafico.render_to_file('prueba.svg')
webbrowser.open('prueba.svg')

Si todo ha ido bien se abrirá tu navegador predeterminado y te mostrará la siguiente imagen:


grafico creado sin datos


Vamos a pararnos un momento para explicar el código. Aunque en teoría para renderizar el gráfico simplemente se podría usar grafico.render()  a la fecha de este post no lo he conseguido en la versión que uso de Debian o Ubuntu. No obtengo ningún error, pero tampoco se renderiza el gráfico. Así que he optado por pasar el gráfico renderizado a un archivo con grafico.renter_to_file('prueba.svg') y luego abrirlo con el navegador para visualizarlo. Para ello he importado la librería webbrowser y luego he utilizado el método open para abrir el grafico creado en el navegador. No obstante también se permite abrir directamente el grafico en el navegador usando grafico.render_in_browser(), si prefieres pero seguramente tendrás que instalar una librería dependiente (lxlm).

Otro inciso antes de seguir es que además de diagramas de barras también podemos usar muchos más tipos de gráficos como líneas, gráficos circulares, etc. Para más información consulta la documentación


Añadiendo algunos datos.


Para almacenar los datos vamos a utilizar las listas de Python. Puedes crearlas poniendo los datos separados por comas entre corchetes [ ]. 

Vamos a crear cuatro listas con datos para mostrar en nuestro gráfico.

Cada lista guardará el nombre de la nación y el número de medallas que ha ganado.

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]
Cuando guardas algo en una lista, cada uno de los elementos tiene un índice. Un índice es un número que nos dice la posición del elemento en la lista. Los índices comienzan en el 0 en vez de en el 1.

Puedes obtener cualquier elemento de una lista a partir de su índice. Por ejemplo, mi_lista[3] nos devolverá el cuarto elemento de esa lista, porque recuerda que el índice comienza en el cero. 

Utiliza los índices de tu lista y grafico.add() para mostrar los datos. El nombre del país que está en el primer elemento de la lista (indice 0) se utilizará como etiqueta de la categoría y la cantidad de medallas que es el segundo elemento de la lista (índice 1) determinará la altura de la barra.

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]

grafico.add(us[0], us[1])
grafico.add(gb[0], gb[1])
grafico.add(fr[0], fr[1])
grafico.add(sp[0], sp[1])

Ejecuta el código para ver el gráfico. 


gráfico creado con algunos datos

Si obtienes un mensaje de ERROR del tipo IndexError, es que tu código esta intentando obtener un valor desde un indice que no existe en la lista (por ejemplo us[2]). Para solucionarlo:

- comprueba cada línea grafico.add() y comprueba que solo estás utilizando los indices 0 y 1.

- comprueba las líneas donde creaste las listas. Verifica que cada lista tenga solo dos elementos, separados por una coma.

Ahora, introduciremos dos nuevos paises, añadiendo nuevas listas y luego las cargaremos en el gráfico usando grafico.add().

# Añadiendo datos
us = ['Estados Unidos', 2655]
gb = ['Gran Bretaña', 931]
fr = ['Francia', 772]
sp = ['España', 169]
ch = ['China', 634]
al = ['Alemania', 797]

grafico.add(us[0], us[1])
grafico.add(gb[0], gb[1])
grafico.add(fr[0], fr[1])
grafico.add(sp[0], sp[1])
grafico.add(ch[0], ch[1])
grafico.add(al[0], al[1])

Vuelve a ejecutar el código para actualizar el gráfico.


gráfico con más datos añadidos


Cargando los datos desde un archivo.


¡Nuestro gráfico tiene buena pinta! Pero, casi 150 naciones han competido en los juegos olimpico de verano. Para hacer un gráfico con todos, vamos a cargar sus datos desde un archivo lo que nos hará ahorrar un montón de tiempo en teclear datos.

Si vas a la carpeta del proyecto en Github encontrarás un archivo csv llamado medals.csv. Ábrelo y echa un vistazo a los datos que contiene.


imagen del archivo csv

Como ves en cada línea aparece el nombre del equipo, y el número de medallas que ha ganado hasta los últimos juegos olímpicos de verano de Tokio 2020. (Oro, plata, bronce y total de medallas)

Información: Archivos CSV
Los archivos CSV (Comma Separated Values) o archivos con valores separados
por comas, contienen datos en filas y columnas como en una tabla. Cada lí-
nea es una fila, en la que los datos (columnas) están separados por comas.

Pais, Abreviatura, Oro, Plata, Bronce, Total

United States,USA,1070,841,744,2655
Soviet Union,URS,395,319,296,1010
Great Britain,GBR,292,324,315,931
Germany,GER,239,267,291,797

Para hacer el gráfico tenemos que volcar la información de cada línea al programa que estamos haciendo, al igual que hicimos con las listas anteriormente.

Para ello vuelve al archivo donde tienes el código del programa y añade el siguiente código que nos permitirá cargar los datos del archivo en una variable, para ello utilizaremos "with open() as". Después mediante un bucle for imprimiremos los datos de cada línea.

El usar el bucle for hace que se repita el código. Así que cargaremos cientos de líneas con los equipos participantes y sus datos ¡con unas pocas líneas de código!

Añade el siguiente código.

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        print(linea)


¿Cómo se lee un archivo con Python?

Para leer un archivo de texto en Python debes abrir el archivo y leer su contenido. 

Cuando abras un archivo, utiliza los comandos with con as. Con esto te asegurarás de que una vez ejecutado el código que contiene, el archivo se cerrará automáticamente. El argumento dentro de open() en este caso 'medals.csv' es el nombre del archivo de texto que queremos abrir. 

Una vez que hemos cargado el archivo, tenemos dos opciones. La primera es volcarlo íntegramente en una variable y una segunda que es usar un bucle for para recorrer el archivo línea por línea.

La primera opción no la hemos usado en este caso pero sería algo como esto:

with open(nombre_archivo) as f:
  archivo_texto = f.read()
  # Hacer algo con el texto

La segunda opción es la que hemos usado nosotros para iterar sobre cada línea del código:

with open(nombre_archivo) as f:
  for línea_archivo in f:
    # hacer algo con la línea.

Si ejecutas ahora el código verás como cada línea tiene seis valores separados por comas.

United States,USA,1070,841,744,2655

Soviet Union,URS,395,319,296,1010

Great Britain,GBR,292,324,315,931

Germany,GER,239,267,291,797

France,FRA,231,256,285,772

People's Republic,of China,CHN,263,199,174,636

Italy,ITA,222,195,215,632

Australia,AUS,162,170,209,541

Hungary,HUN,182,156,177,515

Sweden,SWE,149,177,181,507

Japan,JPN,169,150,180,499

Cada cadena que imprime el bucle se compone de seis valores separados por comas. La función grafico.add() necesita cada una de esas piezas pero como entradas separadas. 

La función split() divide una cadena en una lista, al igual que las listas que hicimos al principio del post. Si usamos split(',') se creara una lista poniendo un nuevo elemento cada vez que encuentre una coma, es decir separa el texto en diferentes valores usando para ello la coma. 

Consejo: split() puede dividir una cadena en una lista usando como separador cualquier texto que se desee, no tiene porque ser una coma, puede ser un punto, una letra e incluso una palabra.

Modifica un poco el código para que recoja esto que hemos comentado:

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        dato = linea.split(',')
        print(dato)

Si lo ejecutas verás lo siguiente:

>>> %Run main.py
['United States', 'USA', '1070', '841', '744', '2655\n']
['Soviet Union', 'URS', '395', '319', '296', '1010\n']
['Great Britain', 'GBR', '292', '324', '315', '931\n']
['Germany', 'GER', '239', '267', '291', '797\n']
['France', 'FRA', '231', '256', '285', '772\n']
["People's Republic", 'of China', 'CHN', '263', '199', '174', '636\n']
['Italy', 'ITA', '222', '195', '215', '632\n']
['Australia', 'AUS', '162', '170', '209', '541\n']
['Hungary', 'HUN', '182', '156', '177', '515\n']
...

Como te habrás dado cuenta cada línea finaliza con "\n" al final. "\n" es normalmente invisible cuando se imprime un texto, por eso no lo has visto antes. Es un salto de línea, le dice al ordenador cuando imprime un texto, que se ha alcanzado el final de la línea.

Sin embargo como para hacer nuestro gráfico necesitamos el primer valor, el nombre del equipo, y el último, el número total de medallas, tenemos que quitar el salto de línea "\n". Para ello usaremos el método strip() que utilizado sin argumentos elimina los espacios en blanco y saltos de línea que pudiera haber tanto al principio como al final de la cadena.

El código sería de momento este:

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        linea = linea.strip()
        dato = linea.split(",")
        print(dato)

Salida:

>>> %Run main.py
['United States', 'USA', '1070', '841', '744', '2655']
['Soviet Union', 'URS', '395', '319', '296', '1010']
['Great Britain', 'GBR', '292', '324', '315', '931']
['Germany', 'GER', '239', '267', '291', '797']
['France', 'FRA', '231', '256', '285', '772']
["People's Republic", 'of China', 'CHN', '263', '199', '174', '636']
['Italy', 'ITA', '222', '195', '215', '632']
['Australia', 'AUS', '162', '170', '209', '541']

Antes de cargar los datos en el gráfico nos queda una cuestión. Todos los valores que nos pasa la cadena son de tipo string. Eso no nos importa para el nombre del equipo puesto que el primer argumento que le pasamos a grafico.add() es un texto para que lo use como etiqueta. Sin embargo para el número total de medallas necesitamos que el dato se un número entero. Podemos usar la función int() para pasar ese dato de tipo texto a número. 

Ten además en cuenta que el nombre del equipo es el primer elemento de la lista (indice 0)  y que el número total de medallas es el elemento seis (indice 5). Con esto ya podemos construir el grafico. El programa completo quedaría de la siguiente forma:

from pygal import Bar
import webbrowser

# Creando un gráfico
grafico = Bar(title="Medallas Olímpicas")

# Añadiendo datos
# us = ['Estados Unidos', 2655]
# gb = ['Gran Bretaña', 931]
# ch = ['China', 634]
# al = ['Alemania', 797]
# fr = ['Francia', 772]
# sp = ['España', 169]

# grafico.add(us[0], us[1])
# grafico.add(gb[0], gb[1])
# grafico.add(fr[0], fr[1])
# grafico.add(sp[0], sp[1])
# grafico.add(ch[0], ch[1])
# grafico.add(al[0], al[1])

with open('medals.csv', 'r') as lineas:
    for linea in lineas:
        linea = linea.strip()
        dato = linea.split(',')
        grafico.add(dato[0], int(dato[5]))
        
        
grafico.render_to_file('prueba.svg')
webbrowser.open('prueba.svg')

Ejecuta el código y mira como se crea el gráfico:


gráfico final del proyecto

¿Qué podemos hacer a mayores?

Para profundizar algo más en la forma de hacer gráficos, podemos cambiar la forma de representar los datos. 

Podríamos por ejemplo:

Crear un gráfico circular -  Para crear un gráfico circular en vez de un gráfico de barras solo tenemos que cambiar el código de importación, para usar Pie en vez de Bar. Una vez importada la creación o instancia del gráfico la haremos usando Pie.

gráfico circular




Puedes encontrar los enlaces a este proyecto en esta dirección de Github.

domingo, 14 de abril de 2024

Documentación de un proyecto.

Basado en el proyecto de la RapsberryPi Org "Documenting your code".

Introducción.

Si has creado un proyecto realmente útil y quiere compartirlo con otra gente, un paso crucial es crear la documentación que ayude a los usuarios a entender que es lo que hace el código, como funciona y como poder usarlo.

Para compartir el código que hayas creado y ayudar a la gente a usarlo, crearemos un sitio web que contenga la documentación de tu proyecto.

Que haremos.

Esta guía te enseñará como construir un sitio web para tu proyecto que automáticamente cree la documentación par tu código Python.

documentación del programa de Python


Que aprenderás.

  • Como documentar el código de Python
  • Como automáticamente generar la documentación.
  • Como usar el reStructuredText markup language.
  • Como crear un proyecto web usando Sphinx.

Empecemos.


Imagina que has creado una obra maestra de software que hará la vida de innumerables programadores un poco más fácil y la has puesto a su disposición, pero nadie lo usa ¡Por que no saben como hacerlo!. Crear documentación es clave cuando compartes tu código.

En este post usaremos como ejemplo el código del proyecto "Mazo de Cartas" para documentarlo. No es esencial que hayas visto este proyecto, pero te sería útil porque te ayudaría a comprender que hace él código que vamos a documentar.

Básicamente el código del programa es el siguiente:

from random import shuffle

class Carta:
    def __init__(self, palo, numero):
        self._palo = palo
        self._numero = numero
    
    # getter y setter para el atributo palo
    @property
    def palo(self):
        return self._palo
    
    @palo.setter
    def palo(self, palo):
        if palo in ["oros", "copas", "espadas", "bastos"]:
            self._palo = palo
        else:
            print("¡Ese no es un palo de la baraja!")
            
    # getter y setter para el atributo numero
    @property
    def numero(self):
        return self._numero
    
    @numero.setter
    def numero(self, numero):
        if isinstance(numero, int):
            if numero in [1,2,3,4,5,6,7,10,11,12]:
                self._numero = numero
            else:
                print("El número de la carta debe estar entre 1 y 12")
                print("Excluyendo el 8 y el 9")
        else:
            print("El valor introducido debe ser númerico")
    
    def esta_en_mazo(self, mazo):
        """Comprueba si una determinada carta esta en el mazo que le
        pasamos o no"""
        for carta in mazo:
            if self.palo == carta.palo:
                if self.numero == carta.numero:
                    return True
        return False
        
            
            
    def __repr__(self):
        return str(self.numero) + " de " + self._palo 
            
class Mazo:    
    def __init__(self):
        self._cartas = []
        self.rellenar()
                
    # Creamos un getter
    @property
    def cartas(self):
        return self._cartas
        
    def rellenar(self):
        palos = ["oros", "copas", "espadas", "bastos"]
        numeros = [1,2,3,4,5,6,7,10,11,12]
        # Devuelve un mazo con las cartas en orden aleatorio.
        cartas = [Carta(p, n) for p in palos for n in numeros]
        shuffle(cartas)
        self._cartas = cartas
        
    def repartir(self, jugadores):
        manos = {jugador: Mano(jugador) for jugador in jugadores}
        for _ in range(3):  # Repartir 3 cartas a cada jugador
            for jugador in jugadores:
                carta = self._cartas.pop()
                manos[jugador].recibir_carta(carta)
        for mano in manos.values():
            mano.mostrar_mano()

class Mano:
    def __init__(self, jugador):
        self.jugador = jugador
        self.cartas = []

    def recibir_carta(self, carta):
        self.cartas.append(carta)

    def mostrar_mano(self):
        print(f"Mano de {self.jugador}:")
        for carta in self.cartas:
            print(carta)

        

mi_mazo = Mazo()
jugadores = ["Jugador 1", "Jugador 2"]
mi_mazo.repartir(jugadores)
Echa un vistazo al código. Verás que hay tres clases: Carta, Mazo y Mano. La clase "Carta" representa una única carta del juego, mientras que "Mazo" es una colección de cartas que primero se crean en un orden y luego pasan a tener uno aleatorio. La clase "Mano" se utiliza para repartir tres cartas a cada jugador.

Al final del programa se crea un mazo de la clase Mazo, se crean dos jugadores y se utiliza un método de la clase mazo para repartir tres cartas a cada uno.

Si ejecutas el programa verás algo parecido a esto:

>>> %Run carta.py
Mano de Jugador 1:
7 de copas
11 de copas
7 de espadas
Mano de Jugador 2:
11 de bastos
11 de espadas
1 de copas

Documentando el código.


Si le echas un vistazo al programa, te darás cuenta de que no hay información adicional que describa como funciona o como se utiliza.

El programa es pequeño, así que probablemente podrías revisar el código e inferir como funciona y que hace cada función. Pero ¿Que pasaría si el programa tuviese miles o millones de líneas de código?. Sería extremadamente difícil y llevaría muchísimo tiempo entender como funciona el programa sin algo de información adicional. 

Python te permite añadir información sobre un programa dentro del código usando docstrings.
Estos son la base para documentar el código. 

Los docstrings se ponen al comienzo del módulo, clase o función en forma de una cadena con tres comillas dobles a cada lado.

def holamundo():
    """ imprime 'hola mundo' en la pantalla """
    print("hola mundo")

Los Docstrings o cadenas de documentación, pueden ser una única línea (como en el ejemplo superior) o pueden ocupar varias.

def holamundo():
    """
    Esta función imprime "hola mundo" en la pantalla.
    No acepta argumentos, ni devuelve nada.
    """
    print("hola mundo")

Tu primera tarea será añadir una cadena de documentación a la clase "Carta" para describir la clase y para que sirve.

class Carta:
    """
    La clase carta representa una única carta y se
    inicializa pasándole el palo y número de la carta a
    representar.
    """
    def __init__(self, palo, numero):

        self._palo = palo
        self._numero = numero

Añade tú una posible cadena de documentación para las otras clases.


Generando la documentación.


Ahora que tu código contiene algo de información sobre si mismo en los formularios de los docstring, puedes usar el módulo de Python pydoc, para que automáticamente se cree un documento HTML con la documentación del código.

Abre una ventana de terminal (Raspberry Pi/Linux PC/Mac) o una ventana de comandos de Windows (Windows PC).

Navega hasta el directorio que contiene el programa carta.py.

cd nombre_del_directorio

Introduce el comando para que el módulo pydoc cree la documentación.

WINDOWS.

python -m pydoc -w .\card.py
Si obtienes un error al ejecutar este comando, consulta la guía uso de Pip en Windows para obtener ayuda con la instalación y asegúrate de haber agregado Python a su ruta.

Raspberry Pi/Linux PC/Mac
python3 -m pydoc -w ./card.py


Cuando el comando ha terminado de ejecutarse, saldrá el mensaje ‘wrote carta.html’. Date cuenta que pydoc usa el nombre del programa de Python como nombre para el archivo HTML.

Abre el archivo carta.html usando tu navegador para ver la documentación que se ha creado.

Verás una página que muestra las clases Carta, Mano y Mazo. Para sus métodos y propiedades, incluyendo la documentación que has añadido como cadenas de documentación.


documentación generada por pydoc


Puedes utilizar esta página HTML sencilla en un servidor Web para proporcionar a tus usuarios información sobre tu código. En los siguientes pasos veremos como usar la herramienta Sphinx para crear un sitio web de documentación al que puedas añadir contenido e información adicional. 


Creando un proyecto web con Sphinx.

Usar pydoc y docstrings es una excelente manera de crear documentación estructurada sobre tu código, pero tiene limitaciones porque no puedes agregar información adicional y contenido.

En un sitio web menos básico, similar al de GPIO Zero, por ejemplo, puedes agregar mucha más información además de la documentación, preguntas frecuentes, imágenes y fragmentos de código.

Usaremos Sphinx para crear un proyecto de sitio web de este tipo. Esta herramienta fue creada originalmente para documentar el lenguaje Python.

Para crear contenido para tu sitio web con Sphinx, escribirás archivos de texto formateados utilizando el lenguaje de marcado simple pero potente reStructuredText (ReST).


Instalar Sphinx.

Usa pip3 para instalar este módulo, por ejemplo:

pip3 install sphinx


Crea un proyecto con Sphinx.


Sphinx incluye una utilidad para crear de forma rápida una plantilla base.

  • Navega al directorio de tu proyecto.

  • Ejecuta el siguiente comando:
sphinx-quickstart




Usa las siguientes respuestas para completar el cuestionario que te realizará la instalación y que determinará las propiedades y la configuración que Sphinx usará para crear la página web.

Son unas pocas preguntas - puedes usar las siguientes respuestas:

Pregunta                                                        Por defecto        Respuesta

- Separar directorios fuente y compilado        n                          n
- Nombre del proyecto                                    n                          carta
- Autor                                                             n                          (nombre autor)
- Liberación del proyecto                                n                          (versión proyecto)
- Lenguaje del proyecto                                  en                         es

El programa de instalación rápida de Sphinx creará unos cuantos archivos y directorios donde se creará tu documentación. Por ejemplo, en Linux se crean los siguientes archivos:

archivos creados por sphinx en linux

Los principales archivos y directorios son:

conf.py - El archivo de configuración de Sphinx que describe como se creará tu documentación. 

index.rst - La página principal e índice de tu documentación.   

_build - El directorio donde se creará la documentación.

Construyendo la página Web.

Para ver como va el proyecto, lo primero que tenemos que hacer es crearlo. Esto lo que hará será convertir los ficheros del proyecto en archivos HTML. 

Ejecuta el siguiente comando para ello:

make html


instalación de sphinx-quickstar


Esto creará un directorio llamado html dentro del directorio _build. Ahí se crearán los archivos html del proyecto. 

Abre el archivo index.html que esta dentro del directorio _build/html usando un navegador web. Verás la página inicial del proyecto, aun sin construir. 

sphinx, inicio html

Lo próximo que haremos será añadir el contenido al proyecto, incluyendo la auto-documentación que antes creamos para el código con pydoc.

Añadiendo la documentación del código.


Configura el archivo conf.py

Para incluir el código de la documentación que creamos antes, Sphinx necesita conocer donde encontrarlo. Se lo diremos modificando el archivo conf.py

Abre el archivo conf.py,  y al comienzo del programa incluye el siguiente código:

# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

import os
import sys
sys.path.insert(0, os.path.abspath('.'))


# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = ['sphinx.ext.autodoc']

Esto añade el actual directorio a la configuración de Sphinx para que pueda encontrar los archivos de código que necesita.

Añadiendo la documentación de tu código.

En ese mismo directorio crea un archivo llamado code.rst. Aquí estará el código para tu página de documentación. 

Añade el título a esta página.

Documentación del Código
=========================


Añade también el siguiente código para importar el módulo carta.py

.. module:: carta

A continuación añade lo siguiente para añadir la documentación para las clases Carta, Mano y Mazo.

.. autoclass:: Carta
    :members:
.. autoclass:: Mano
    :members:
.. autoclass:: Mazo
    :members:

Este archivo debe quedar como este:

Documentación del Código
=========================
.. module:: carta
.. autoclass:: Carta
    :members:
.. autoclass:: Mano
    :members:
.. autoclass:: Mazo
    :members:


Para que lo que hemos especificado en este archivo, code.rst aparezca en la página web de tu proyecto, tenemos que añadirlo al índice, para lo cual tienes que abrir el archivo index.rst y modificarlo, añadiendo la página "code" justo debajo de en la tabla de contenidos ..toctree:: - El resultado debería ser algo como esto:

¡Bienvenido a la documentación de carta!
==========================================

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   code
Vuelve a reconstruir el proyecto usando el comando:

make html 

Con lo que la página del código aparecerá ahora en el índice. Para ver si todo funciona correctamente vuelve a abrir el directorio _build/html y abre de nuevo el archivo index.html en el navegador. 

La página "code" que hemos creado aparecerá ahora en el índice.


pagina de inicio con el código añadido


Cuando abres el enlace "documentación del código" verás la documentación autogenerada para cada una de las clases del módulo.

documentación de las clases del módulo


Creando páginas.


Además de las páginas que describen el código del programa, puedes incluir otras con otro contenido dentro del proyecto web.

Todas las páginas que quieras añadir tienen que estar formateadas usando el lenguaje Rest markup, Este enlace te mostrará como usar las características más comunes de este lenguaje. 

Para empezar crearemos una página "acerca de" que mostrará algunos detalles del proyecto y donde encontrar más información.

Crea un nuevo archivo en el mismo directorio llamado acerca_de.rst.

Luego comenzaremos añadiendo el título de la nueva página.

Sobre este Proyecto.
==================

Luego añadiremos algo de texto que describa el proyecto, por ejemplo:

Mazo de cartas es un conjunto de clases para crear naipes de una baraja. 

Quizá será bueno añadir un enlace al código fuente del proyecto:

Puedes crear este proyecto tu mismo visitando "esta página <https://projects.raspberrypi.org/en/projects/deck-of-cards>"_.


Información: URLs in ReST

La estructura de una URL en lenguaje markup es muy específica en ReST.
El texto del link y el enlace mismo necesitan estar entre dos guiones
simples, el segundo de los cuales tiene que estar seguido por un guion 
bajo, de esta forma:
'texto_del_link <url>'_

Es importante dejar un espacion en blanco entre el texto del link y su 
enlace.

Este tipo de marcado se denomina en línea porque está dentro del texto.

También puede crear definiciones de URL independientes y luego hacer 
referencia a ellas en cualquier parte del texto; esto es útil si desea 
vincular a la misma URL dos veces.

Por favor 'raspberry pi'_.

.. _raspberry pi: https://raspberrypi.org/

Cuando hayas terminado de crear la página "acerca_de", añadela a la pagina del índice "index.rst"

¡Bienvenido a la documentación de carta!
==========================================

.. toctree::
   :maxdepth: 2
   :caption: Contents:

   code
   acerca_de 

Reconstruye el proyecto de nuevo con la instrucción:

make html

página acerca de

Como reto personal puedes añadir nuevas páginas al proyecto como por ejemplo una de preguntas frecuentes.


Desafío: Mejorar la página Web

  1. Investiga las diferentes opciones que Sphinx no facilita para personalizar nuestro proyecto, como por ejemplo el cambio de tema
  2. Puedes documentar mejor el programa añadiendo los docstring que consideres oportuno.
  3. Aloja tu proyecto en Github o en Read the Docs

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

miércoles, 10 de abril de 2024

Mazo de cartas.

Basado en el proyecto de la RapsberryPi Org "Deck of cards"

En este post crearemos un modelo de baraja o mazo de cartas que puede ser la base para programar juegos de cartas en Python como "la brisca", "el mus" o "el tute"

¿Que es lo que haremos?

Aprenderemos como usar la programación basada en objetos para crear un modelo reusable de mazos de cartas.

La programación basada en objetos (OOP en inglés OBJECT-ORIENTED PROGRAMMING) es una forma de organizar el código, para que sea más fácil de entender, volverlo a usar y cambiarlo. OPP nos permite combinar datos (varibles) y funcionalidades y combinarlas dentro de los objetos.


¿Cómo crear una clase?

Una clase es como una plantilla para crear los objetos. Piensa en las clases como en un molde para hacer galletas. Todas las galletas que obtengamos habrán salido de ese molde. Cuando hacemos galletas con un molde, aunque todas se hacen con el mismo instrumento, se pueden personalizar. Por ejemplo añadiendo virutas de chocolate o canela. De la misma forma puedes personalizar los objetos creados por una clase al guardar diferentes datos en el. Vamos a verlo en la práctica.

Vamos a comenzar creando una clase llamada Carta que actuará como plantilla para crear las cartas de la baraja. Cada objeto Carta es una instancia separada de la clase Carta. Por ejemplo, podrías tener un objeto carta que representara el 5 de espadas y otro objeto carta distinto que representara el 2 de bastos.



Crea un nuevo archivo de Python y llámalo cartas.py

Crea una nueva clase llamada Carta

class Carta:

Los nombres de las clases generalmente se escriben con letra mayúsculas para diferenciarlos fácilmente de los nombres de las variables.

Lo próximo que haremos será crear un método a esta clase. Los métodos son muy similares a las funciones y los usaremos para interactuar con los objetos.


Métodos.

Seguramente ya te hayas encontrado con funciones al escribir código Python. Las funciones nos permiten asignar un nombre a un conjunto de instrucciones. Puedes pasar datos a las mismas usando parámetros y opcionalmente te pueden devolver algún tipo de dato como resultado. 

La diferencia entre una función y un método es que este último es llamado por un objeto. Esto significa que ese método puede usar todos los datos guardados dentro del objeto, así como cualquier dato que le pases a través de parámetros.


Crear un método __init__

En Python, cada clase tiene un método especial llamado __init__, nos solemos referir a el como método constructor de la clase, ya que nos dice como crear o inicializar un objeto. Este método tan particular siempre tiene dos guiones bajos tanto antes como después de su nombre.

  • Crea un método __init__ dentro de tu clase Carta.

class Carta:
    def __init__(self):


Información: ¿Por qué necesito el "self" dentro de los paréntesis?
Un método necesita un contexto para poder funcionar. "self" es la referencia
al objeto, y tiene que ser el primer parámetro que se pase a cualquier méto-
do de la clase. Esto es así porque el método necesita saber quien le está
llamando y así poder usar los datos que están almacenados en el objeto.

Pongamos un ejemplo:

Fuera de la programación orientada a objetos (OOP) para que dos funciones
compartan una misma variable, esta debe ser global. Por ejemplo:
name = "Laura"

def hola():
    print("Hola " + name)

def adios():
    print("Adios " + name)
Dentro de una clase, puede ser usado para compartir variables. Por ejemplo:
class Bienvenido():
    def __init__(self):
        self.name = "Laura"

    def hola(self):
        print("Hola " + self.name)

    def adios(self):
        print("Adios " + self.name)
En este ejemplo, hemos definido la variable self.name y establecido su valor a "Laura" dentro del método __init__ que construye el objeto. De esta forma todos los objetos que obtengamos de la clase tendrán una variable self.name con el valor de "Laura". Los métodos hola() y adios() que hemos definido, pueden hacer uso de la informaicón almacenada en la variable self.name.


Atributos.


Los atributos son pedazos de información guardados dentro de un objeto, los podríamos definir más bien como variables asociadas a ese objeto. El objeto carta comenzará teniendo dos atributos, palo y número y les antepondremos el prefijo self. para mostrar que pertenecen al objeto instanciado o creado de la clase.

  • Añade dos atributos a tu método __init__  y dos parámetros de tal forma que puedas pasar sus valores como argumento cuando creemos el objeto:
def __init__(self, palo, numero):
    self.palo = palo
    self.numero = numero

Instanciar un Objeto.


Vamos a probar nuestra clase creando un primer objeto. El objeto es una instancia de la clase Carta, por lo que al crearlo así se le llama instanciar.

  • Debajo de la definición de la clase, instancia un objeto carta, por ejemplo para el cinco de espadas y llámalo "mi_carta".

mi_carta = Carta("espadas", 5)

  • Añade la función print para mostrar como se ve el objeto que hemos creado.
print(mi_carta)

  • Ejecuta el programa.

Probablemente esperabas ver en la salida algo que pusiera "espadas" y un 6. En lugar de esto lo que ves en su lugar es la representación del objeto creado y concretamente su dirección en la memoria del ordenador.

Python 3.10.12 (/home/user/Escritorio/PROYECTOS/miEntorno/bin/python3)
>>> %cd /home/user/Escritorio/PROYECTOS/Mazo_de_Cartas
>>> %Run carta.py
<__main__.Carta object at 0x7d3330e9f850>
Esta salida es debido a un método especial llamado __repr__ (forma abreviada de representación). Todos los objetos en Python tienen este método por defecto, lo que significa que no necesitas crearlo por ti mismo. Se usará automáticamente cuando le digas a un programa que devuelva una representación de texto de un objeto. Sin embargo, puedes sobreescribir la salida por defecto de este método para cambiar la forma de ver el objeto al representarlo como texto.

  • Vuelve a la definición de la clase Carta y añade el siguiente código para sobrescribir el método __repr__ y que nuestro objeto carta se muestre de una forma más legible.
def __repr__(self):
    return str(self.numero) + " de " + self.palo

En nuestro ejemplo, si el palo son "espadas" y la carta es el número 5 se imprimirá el "5 de espadas"

  • Ejecuta el programa y comprueba que todo funciona correctamente.
>>> %Run carta.py
5 de espadas

Atributos y Propiedades.


Puedes cambiar los atributos de un objeto, para lo cual tu clase necesitará usar las propiedades.

Las propiedades son métodos especiales que obtienen o establecen el valor de un atributo y normalmente se les suele designar con el nombre de getter o setter.

Métodos Getter o Setter.


Es posible acceder a los atributos de un objeto directamente. Por ejemplo, tu podrías añadir el siguiente código en la parte de abajo de tu programa para cambiar el atributo "palo" de mi_carta y luego mostrar el objeto.

my_carta.palo = "enigma"
print(mi_carta)

El código del programa se vería de esta forma:


class Carta:
    def __init__(self, palo, numero):
        self.palo = palo
        self.numero = numero
        
    def __repr__(self):
        return str(self.numero) + " de " + self.palo
        
mi_carta = Carta('espadas', 5)
mi_carta.palo = "enigma"
print(mi_carta)
SALIDA:

>>> %Run carta.py
5 de enigma

Sin embargo, acceder a los atributos directamente no es una buena idea, porque el usuario de tu programa podría acceder al atributo directamente y estropear los palos de la baraja, como hemos visto en el ejemplo previo.

En su lugar crearemos propiedades para las clases: getter y setter para acceder al atributo palo.

  • Antes de empezar, regresa al método __init__ de la clase y localiza el atributo self.palo. Añade un guión bajo delante de palo para indicar que no quieres que la gente pueda acceder al atributo directamente.

def __init__(self, palo, numero):
    self._palo = palo
Información: ¿Poner el guión bajo hace que el atributo no sea acesible
directamente?

Añadir un guión bajo es una buena práctica y un buen estilo de programación.

Sin embargo, el guion bajo no previene de que el usuario pueda cambiar el
atributo directamente - es una convención que se usa para indicar que no
deberían hacerlo. Si quieres ver esto, añade un guión bajo a tu código, para
cambiar el atributo a "dinosaurios".

my_card._palo = "dinosaurios"
Verás como puedes cambiar el atributo al igual que anteriormente.


Creando un Getter.

Regresa a la definición de la clase Carta y añade un nuevo método llamado "palo" y haz que devuelva el valor del atributo _palo.

def palo(self):
    return self._palo

Añade un decorador a este método para decirle que es una propiedad.

@property
def palo(self):
    return self._palo

Ahora, cada vez que alguien use el valor mi_carta.palo en su programa, se llamará a este getter, y el usuario recibirá el valor de self._palo almacenado dentro del objeto mi_carta.

Información: ¿Que es un decorador?

En la programación orientada a objetos, los decoradores te permiten añadir
nuevas características o funcionalidades a las clases. 

Un decorador se puede considerar como un contenedor de un método. Contiene 
al método pero también puede extender su funcionalidad.

El decorador @property en Python se usa para convertir un método getter en 
una propiedad.


Creando un setter.

Añade otro método. Es importante que este método tambien se llame "palo". Se debe tomar un dato que represente el nuevo palo que el usuario quiere establecer y realizar una comprobación básica de que ese palo sea uno de los habituales de una baraja.

def palo(self, palo):
    if palo in ["oros", "copas", "espadas", "bastos"]:
        self._palo = palo
    else:
        print("¡Ese no es un palo de la baraja!")

Ahora añade un decorador a este método para decirle que es el setter de "palo".

Al igual que el método getter, este decorador define el método como una propiedad. Ahora cuando alguien quiera establecer un valor para el palo de la carta (por ejemplo tecleando mi_carta.palo = "espadas"), se llamará a la propiedad setter. En este ejemplo el valor "espadas" se pasará como el parámetro palo.

Observa que mediante el uso de propiedades de getter y setter, así como decoradores, puedes tener dos funciones con el mismo nombre, una que se llama cuando obtienes el valor y otra que se llama cuando estableces el valor.

Información: ¿Por qué usamos las propiedades?

¿Por qué querríamos usar los decoradores @property y .setter para crear las
propiedades en lugar de simplemente usar un método llamado obtener_palo()
o establecer_palo()?

Hay varias razones:
  • Es más corto y bonito poder usar carta.palo en vez de carta.obtener_palo() o carta.establecer_palo() por ejemplo.
    mi_palo = carta.palo
    carta.palo = "espadas"
    Es mucho más ordenado que:
    mi_palo = carta.obtener_palo()
    carta.establecer_palo("espadas")
  • Puedes hacer que las funciones complejas parezcan operaciones simples.
  • Si usas propiedades en lugar de permitir el acceso directo a los atributos y necesitas cambiar cómo funciona la clase, puedes hacerlo sin romper ningún código que use la clase. Por ejemplo, el setter original de la variable "palo" simplemente almacenaba la carta, pero supongamos que ahora quieres almacenarla en mayúsculas. Para hacerlo, simplemente puedes cambiar el código dentro de la propiedad:
    @palo.setter
    def palo(self, palo):
            if palo in ["oros", "copas", "espadas", "bastos"]:
                self._palo = palo.upper()
            else:
                print("¡Ese no es un palo de la baraja¡")
    Ahora todas las cartas se almacenarán en mayúsculas, mientras que cualquier código que utilice la propiedad "suit" seguirá funcionando. Si permites que las personas accedan al atributo "suit" directamente, no podrás cambiar ningún aspecto de tu código más adelante.
Ejecuta el programa. Si intentas cambiar el palo de la carta a algo que no sea uno de los palos en la lista, deberías ver "¡Ese no es un palo de la baraja!" y el palo no debería cambiar.

Ten en cuenta que actualmente no tienes ninguna validación en el método __init__, por lo que aún podrías crear el 5 de Dinosaurios de la siguiente manera:

otra_carta = Carta("Dinosaurios", "2")

El programa hasta el momento sería algo como esto:

class Carta:
    def __init__(self, palo, numero):
        self._palo = palo
        self.numero = numero
    
    @property
    def palo(self):
        return self._palo
    
    @palo.setter
    def palo(self, palo):
        if palo in ["oros", "copas", "espadas", "bastos"]:
            self._palo = palo
        else:
            print("¡Ese no es un palo de la baraja!")
        
    def __repr__(self):
        return str(self.numero) + " de " + self._palo
        
mi_carta = Carta('espadas', 5)
mi_carta.palo = "enigma"
print(mi_carta.palo)


Un reto: añade un getter y un setter.


Ahora es tu turno. Basándote en los pasos previos añade un getter y un setter para el atributo numero. 

No olvides:

  • En un mazo de cartas de la baraja española tradicional, cada carta tendrá un número que va del 1 al 12, pero sin contar con los números 8 y 9. Intenta introducir esto en el método setter para que se verifique que se introduce un valor númerico y que este dentro del rango permitido.


Un mazo de cartas.


Ahora que tenemos un modelo para crear las cartas es hora de que creemos un mazo. 

  • Crea una clase Mazo. Puedes crearlo en el mismo archivo que la otra clase o en otro diferente. Si lo haces en un archivo diferente (Por ejemplo mazo.py) necesitaras importar la clase Mazo al principio del archivo con este código:

from mazo import Mazo

En esta línea de código, "mazo" es el nombre del archivo de Python que contiene la clase menos la extension .py, y "Mazo" es el nombre de la clase.

  • Crea una nueva clase llamada "Mazo" e incluye el constructor (__init__) en el. En esta ocasión no necesitaremos añadir ningun parámetro adicional, solamente el self.

class Mazo:

    def __init__(self):

El mazo necesitará almacenar una lista de cartas, cada una de las cuales será un objeto carta. Añade un atributo llamado _cartas al constructor y defínelo como una lista vacía por el momento.

Ahora escribiremos un método para llenar el mazo con las 40 cartas necesarias. Empieza por crear un método llamado "rellenar".

def rellenar(self):


Dentro del método crea dos listas. Una debería contener todos los posibles palos de la baraja y la otra todos los posibles números.

def rellenar(self):
    palos = ["oros", "copas", "espadas", "bastos"]
    numeros = [1,2,3,4,5,6,7,10,11,12]
Información: ¿Hay una forma más eficiente de generar una lista de números?

¡Pues si! En lugar de escribir todos los números, podríamos usar lo que se
llama compresión de listas, que es una forma de crear una nueva lista,
basándonos en una existente.

Asi que para crear una lista que contenga todos los números del 1 al 12, 
pero excluyendo los números 8 y 9, podemos usar el siguiente código:
numeros = [n for n in range(1,13) if n not in [8, 9]]
Esto significa que cada n en el rango de 1 al 13 es incluido en la nueva lista, siempre que no sean el 8 o el 9. Recuerda que la función range() comenzará en el 1 y parará (pero no incluirá) en el 13.

Así que para que el método rellenar genere el mazo de carta tenemos que combinar elementos de las dos listas. Para cada palo y para cada número hay que crear un objeto. Una forma de hacerlo es mediante bucles anidados:

cartas = [] # Crea una lista vacia de cartas
        for palo in palos: # Por cada palo
            for numero in numeros: # Por cada número
                # Creamos un nuevo objeto Carta y lo añadimos a la lista.
                cartas.append(Carta(palo, numero))
        self._cartas = cartas # Apuntamos a esta lista con self._cartas

Sin embargo, el usar bucles anidados puede hacer que nuestro código se vea complicado. Para simplificarlo podemos usar compresión de listas.

self._cartas = [ Carta(s, n) for p in palos for n in numeros ]

Si te gustaría leer más sobre la compresión de listas, echa un vistazo a la siguiente información.

Información: La compresión de listas en Python de una forma sencilla.

Si quieres generar una lista usando Python, es bastante fácil de hacer
usando un bucle for.
nueva_lista = []
for i in range(10):
    nueva_lista.append(i)
>>> nueva_lista
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • La misma lista puede ser creada con una sola línea usando un constructor que existe en muchos lenguajes de programación: una compresión de lista.
nueva_lista = [i for i in range(10)]
>>> nueva_lista
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • Puedes usar cualquier iterable en una compresión de listas, así que hacer una lista a partir de otra es muy fácil.
numeros = [1, 2, 3, 4, 5]
numeros_copia = [numero for numero in numeros]
>>> numeros_copia
[1, 2, 3, 4, 5]
  • También puedes hacer cálculos en una compresión de listas.
numeros = [1, 2, 3, 4, 5]
doble = [numero * 2 for numero in numeros]
>>> doble
[2, 4, 6, 8, 10]
  • También se pueden hacer operaciones con strings.
verbos = ['gritar', 'caminar', 'ver']
futuro = [word + 'é' for word in verbs]
>>> futuro
['gritaré', 'caminaré', 'veré']
Puedes añadir elementos a las lista de forma sencilla.
animales = ['gato', 'perro', 'pez']
animales = animales + [animal.upper() for animal in animales]
>>> animals
['gato', 'perro', 'pez', 'GATO', 'PERRO', 'PEZ']

Vamos a comprobar si nuestro método rellenar genera un mazo correctamente. Vuelve al método __init__, llama al método rellenar() y luego imprime la lista de las cartas.

def __init__(self):
    self._cartas = []
    self.rellenar()
    print(self._cartas)

Crea una instancia de la clase Mazo para comprobar si se genera el mazo tal como esperabas. El programa tendrá un aspecto parecido a este:

class Carta:
    def __init__(self, palo, numero):
        self._palo = palo
        self._numero = numero
    
    # getter y setter para el atriburo palo
    @property
    def palo(self):
        return self._palo
    
    @palo.setter
    def palo(self, palo):
        if palo in ["oros", "copas", "espadas", "bastos"]:
            self._palo = palo
        else:
            print("¡Ese no es un palo de la baraja!")
            
    # getter y setter para el atriburo numero
    @property
    def numero(self):
        return self._numero
    
    @numero.setter
    def numero(self, numero):
        if isinstance(numero, int):
            if numero in [1,2,3,4,5,6,7,10,11,12]:
                self._numero = numero
            else:
                print("El número de la carta debe estar entre 1 y 12")
                print("Excluyendo el 8 y el 9")
        else:
            print("El valor introducido debe ser númerico")
            
            
    def __repr__(self):
        return str(self.numero) + " de " + self._palo 
            
class Mazo:    
    def __init__(self):
        self._cartas = []
        self.rellenar()
        print(self._cartas)
        
    def rellenar(self):
        palos = ["oros", "copas", "espadas", "bastos"]
        numeros = [1,2,3,4,5,6,7,10,11,12]
#         cartas = [] # Crea una lista vacia de cartas
#         for palo in palos: # Por cada palo
#             for numero in numeros: # Por cada numero
#                 # Creamos un nuevo objeto Carta y lo añadimos a la lista.
#                 cartas.append(Carta(palo, numero))
#         self._cartas = cartas # Apuntamos a esta lista con self._cartas
        self._cartas = [ Carta(p, n) for p in palos for n in numeros ]
mi_mazo = Mazo()

La salida será algo como esto:

>>> %Run carta.py
[
    1 de oros,
    2 de oros,
    3 de oros,
    4 de oros,
    5 de oros,
    6 de oros,
    7 de oros,
    10 de oros,
    11 de oros,
    12 de oros,
    1 de copas,
    2 de copas,
    3 de copas,
    4 de copas,
    5 de copas,
    6 de copas,
    7 de copas,
    10 de copas,
    11 de copas,
    12 de copas,
    1 de espadas,
    2 de espadas,
    3 de espadas,
    4 de espadas,
    5 de espadas,
    6 de espadas,
    7 de espadas,
    10 de espadas,
    11 de espadas,
    12 de espadas,
    1 de bastos,
    2 de bastos,
    3 de bastos,
    4 de bastos,
    5 de bastos,
    6 de bastos,
    7 de bastos,
    10 de bastos,
    11 de bastos,
    12 de bastos
]


Retos: ¿Que más?

Ahora que has creado las clases Carta y Mazo. ¿Qué más cosas podrías introducir para usarlo en un programa de cartas? Aquí te dejo algunas ideas.

  • Añade un nuevo método para que las cartas en el mazo aparezcan en orden aleatorio.
  • Crea un método que comprueba si una determinada carta esta en el mazo o no.
  • Un método llamado repartir() que reparta las cartas a los jugadores. ¿Cuántas cartas entregarás a cada jugador?
  • ¿Podrías crear una clase llamada "Mano" para modelar las cartas de cada jugador?

Puede encontrar el código de este proyecto en el siguiente enlace de Github.