Clase 9: Control de versiones y módulos


Nota

Esta clase está copiada (muy fuertemente) inspirada en las siguientes fuentes:

¿Qué es y para qué sirve el control de versiones?

El control de versiones permite ir grabando puntos en la historia de la evolución de un proyecto. Esta capacidad nos permite:

  • Acceder a versiones anteriores de nuestro trabajo («undo ilimitado»)

  • Trabajar en forma paralela con otras personas sobre un mismo documento.

Habitualmente, nos podemos encontrar con situaciones como esta:

o, más gracioso pero igualmente común, esta otra:

“Piled Higher and Deeper” por Jorge Cham, http://www.phdcomics.com

Todos hemos estado en esta situación alguna vez: parece ridículo tener varias versiones casi idénticas del mismo documento. Algunos procesadores de texto nos permiten lidiar con esto un poco mejor, como por ejemplo el Track Changes de Microsoft Word, el historial de versiones de Google, o el Track-changes de LibreOffice.

Estas herramientas permiten solucionar el problema del trabajo en colaboración. Si tenemos una versión de un archivo (documento, programa, etc) podemos compartirlo con los otros autores para modificar, y luego ir aceptando o rechazando los cambios propuestos.

Algunos problemas aún aparecen cuando se trabaja intensivamente en un documento, porque al aceptar o rechazar los cambios no queda registro de cuáles eran las alternativas. Además, estos sistemas actúan sólo sobre los documentos; en nuestro caso puede haber datos, gráficos, etc que cambien (o que queremos estar seguros que no cambiaron y estamos usando la versión correcta).

En el fondo, la manera de evitar esto es manteniendo una buena organización. Una posible buena manera es designar una persona responsable, que vaya llevando la contabilidad de quién hizo qué correcciones, las integre en un único documento, y vaya guardando copias de todas los documentos que recibe en un lugar separado. Cuando hay varios autores (cuatro o cinco) éste es un trabajo bastante arduo y con buenas posibilidades de pequeños errores. Los sistemas de control de versiones tratan de automatizar la mayor parte del trabajo para hacer más fácil la colaboración, manteniendo registro de los cambios que se hicieron desde que se inició el documento, y produciendo poca interferencia, permitiendo al mismo tiempo trabajar de manera similar a como lo hacemos habitualmente.

Consideremos un proyecto con varios archivos y autores. En este esquema de trabajo, podemos compartir una versión de todos los archivos del proyecto

Cambios en paralelo

Una de las ventajas de los sistemas de control de versiones es que cada autor hace su aporte en su propia copia (en paralelo)

versiones corregidas en paralelo

y después estos son compatibilizados e incorporados en un único documento.

Compaginando versiones corregidas en paralelo

En casos en que los autores trabajen en zonas distintas la compaginación se puede hacer en forma automática. Por otro lado, si dos personas cambian la misma frase obviamente se necesita tomar una decisión y la compaginación no puede (ni quiere) hacerse automáticamente.

Historia completa

Otra característica importante de los sistemas de control de versiones es que guardan la historia completa de todos los archivos que componen el proyecto. Imaginen, por ejemplo, que escribieron una función para resolver parte de un problema. La función no sólo hace su trabajo sino que está muy bien escrita, es elegante y funciona rápidamente. Unos días después encuentran que toda esa belleza era innecesaria, porque el problema que resuelve la función no aparece en su proyecto, y por supuesto la borra. La version oficial no tiene esa parte del código.

Dos semanas, y varias líneas de código, después aparece un problema parecido y querríamos tener la función que borramos …

Los sistemas de control de versiones guardan toda la información de la historia de cada archivo, con un comentario del autor. Este modo de trabajar nos permite recuperar (casi) toda la información previa, incluso aquella que en algún momento decidimos descartar.

Instalación

Vamos a describir uno de los posibles sistemas de control de versiones, llamado git

En Linux, usando el administrador de programas, algo así como:

en Ubuntu:

$ sudo apt-get install git

o usando dnf en Fedora:

$ sudo dnf install git

En Windows, se puede descargar Git for Windows desde este enlace, ejecutar el instalador y seguir las instrucciones. Viene con una terminal y una interfaz gráfica.

Interfaces gráficas

Existen muchas interfaces gráficas, para todos los sistemas operativos.

Ver por ejemplo Git Extensions, git-cola, Code Review, o cualquiera de esta larga lista de interfaces gráficas a Git.

Documentación

Buscando en internet el término git se encuentra mucha documentación. En particular el libro Pro Git tiene información muy accesible y completa.

El programa se usa en la forma:

$ git <comando> [opciones]

Por ejemplo, para obtener ayuda directamente desde el programa, se puede utilizar cualquiera de las opciones:

$ git help
$ git --help

que nos da información sobre su uso, y cuáles son los comandos disponibles. Si queremos obtener información sobre un comando en particular, agregamos el comando de interés. Para el comando de configuración sería:

$ git config --help
$ git help config

Configuración básica

Una vez que está instalado, es conveniente configurarlo desde una terminal, con los comandos:

$ git config --global user.name "Juan Fiol"
$ git config --global user.email "fiol@cab.cnea.gov.ar"

Si necesitamos usar un proxy para acceder fuera del lugar de trabajo:

$ git config --global http.proxy proxy-url
$ git config --global https.proxy proxy-url

Acá hemos usado la opción –global para que las variables configuradas se apliquen a todos los repositorios con los que trabajamos.

Si necesitamos desabilitar una variable, por ejemplo el proxy, podemos hacer:

$ git config --global unset http.proxy
$ git config --global unset https.proxy

Creación de un nuevo repositorio

Si ya estamos trabajando en un proyecto, tenemos algunos archivos de trabajo, sin control de versiones, y queremos empezar a controlarlo, inicializamos el repositorio local con:

$ git init

Este comando sólo inicializa el repositorio, no pone ningún archivo bajo control de versiones.

Clonación de un repositorio existente

Otra situación bastante común ocurre cuando queremos tener una copia local de un proyecto (grupo de archivos) que ya existe y está siendo controlado por git. En este caso utilizamos el comando clone en la forma:

$ git clone <url-del-repositorio> [nombre-local]

donde el argumento nombre-local es opcional, si queremos darle a nuestra copia un nombre diferente al que tiene en el repositorio

Ejemplos:

$ git clone /home/fiol/my-path/programa
$ git clone /home/fiol/my-path/programa programa-de-calculo
$ git clone https://github.com/fiolj/intro-python-IB.git
$ git clone https://github.com/fiolj/intro-python-IB.git clase-ib

Los dos primeros ejemplos realizan una copia de trabajo de un proyecto alojado también localmente. En el segundo y cuarto casos les estamos dando un nuevo nombre a la copia local de trabajo.

En los últimos tres ejemplos estamos copiando proyectos alojados en repositorios remotos, cuyo uso es bastante popular: bitbucket, gitlab, y github.

Lo que estamos haciendo con estos comandos es copiar no sólo la versión actual del proyecto sino toda su historia. Después de ejecutar este comando tendremos en nuestra computadora cada versión de cada uno de los archivos del proyecto, con la información de quién hizo los cambios y cuándo se hicieron.

Una vez que ya tenemos una copia local de un proyecto vamos a querer trabajar: modificar los archivos, agregar nuevos, borrar alguno, etc.

Ver el estado actual

Para determinar qué archivos se cambiaron utilizamos el comando status:

$ cd my-directorio
$ git status

Creación de nuevos archivos y modificación de existentes

Después de trabajar en un archivo existente, o crear un nuevo archivo que queremos controlar, debemos agregarlo al registro de control:

$ git add <nuevo-archivo>
$ git add <archivo-modificado>

Esto sólo agrega la versión actual del archivo al listado a controlar. Para incluir una copia en la base de datos del repositorio debemos realizar lo que se llama un «commit»

$ git commit -m "Mensaje para recordar que hice con estos archivos"

La opción -m y su argumento (el string entre comillas) es un mensaje que dejamos grabado, asociado a los cambios realizados. Puede realizarse el commit sin esta opción, y entonces git abrirá un editor de texto para que escribamos el mensaje (que no puede estar vacío).

Actualización de un repositorio remoto

Una vez que se añaden o modifican los archivos, y se agregan al repositorio local, podemos enviar los cambios a un repositorio remoto. Para ello utilizamos el comando:

$ git push

De la misma manera, si queremos obtener una actualización del repositorio remoto (poque alguien más la modificó), utilizamos el (los) comando(s):

$ git fetch

Este comando sólo actualiza el repositorio, pero no modifica los archivos locales. Esto se puede hacer, cuando uno quiera, luego con el comando:

$ git merge

Estos dos comandos, pueden generalmente reemplazarse por un único comando:

$ git pull

que realizará la descarga desde el repositorio remoto y la actualización de los archivos locales en un sólo paso.

Puntos importantes

Control de versiones

Historia de cambios y «undo» ilimitado

Configuración

git config, con la opción –global

Creación

git init inicializa el repositorio

git clone copia un repositorio

Modificación

git status muestra el estado actual

git add pone archivos bajo control

git commit graba la versión actual

Explorar las versiones

git log muestra la historia de cambios

git diff compara versiones

git checkout recupera versiones previas

Comunicación con remotos

git push Envía los cambios al remoto

git pull copia los cambios desde remoto

Importando módulos

Python tiene reglas para la importación de módulos a nuestro código. Un módulo es un archivo cuya extensión es .py. Supongamos que tenemos un módulo rotacion_p.py.

  1. En primer lugar, el intérprete busca un módulo denominado rotación_p dentro de los módulos incorporados automáticamente. La lista de dichos módulos se encuentra usando el método sys.builtin_module_names:

import sys
sys.builtin_module_names
'rotacion_p' in sys.builtin_module_names
  1. En segundo lugar, busca un archivo rotacion_p.py en una lista de directorios dada por el atributo sys.path.

sys.path

Este atributo contiene el directorio local (el primero que aparece en la lista de arriba), y una serie de directorios que provienen de - La variable de entorno PYTHONPATH - Un directorio dependiente de la instalación (en este caso, ’/Users/flavioc/miniconda3/envs/clases/lib/python3.11`).

La manera pythonística de chequear si la variable de entorno PYTHONPATH existe es utilizar el método get de os.environ:

import os
os.environ.get('PYTHONPATH')

En nuestro caso no imprime nada, pero de la misma forma se puede setear dicha variable de entorno:

os.environ['PYTHONPATH'] = '..' # seteo la variable PYTHONPATH al directorio padre del directorio actual
os.environ.get('PYTHONPATH')

Por supuesto, todo va a depender de cómo tenemos estructurado nuestro código. En principio, aún cuando uno no utilice completamente las facilidades de Python como lenguaje orientado a objeto, agrupamos funciones que están relacionadas entre sí en distintos módulos. A medida que el código crece, es posible organizar los distintos módulos distribuyéndolos en directorios.

Importando módulos de directorios hijos

Imaginemos que tenemos la siguente estructura de código:

/miproyecto
    main.py
    /lib
        rotacion.py
    /graficos
        simple.py
        complejo.py
        /tresd
        vector.py

Es sencillo importar los módulos en los directorios hijos (lib, graficos):

from graficos.simple import plot_data
from graficos.complejo import plot_data_complejo as plot_complejo
from graficos.tresd.vector import *
from lib.rotacion import rotate

Básicamente exponemos el módulo usando el operador . para ir incorporando los hijos. Por ejemplo, graficos.tresd.vector refiere al módulo que se encuentra en el archivo vector.py en el directorio graficos.tresd.

Importando módulos desde padres o hermanos

Imports relativos

Para importar módulos que están en directorios padres o hermanos (estos últimos son directorios al mismo nivel del directorio desde el cual quiero importar) podemos diferentes estrategias. La primera de ellas es usar la importación de paquetes relativos. Para ello, cada directorio desde el que quiera importar debe poseer un archivo (en principio vacío) denominado __init__.py. Esto permite a Python reconocer los directorios que contienen módulos aún cuando sean padres o hermanos.

Veamos ahora la estructura de directorios de miproyecto_relativo con los archivos __init__.py agregados:

/miproyecto
    main.py
    __init__.py
    /lib
        rotacion.py
        __init__.py
    /graficos
        __init__.py
        simple.py
        complejo.py
        /tresd
        __init__.py
        vector.py
    /tests
        test_rotacion.py

Notemos que tests/test_rotacion.py tiene también un main, que corre un test:

def test_rotacion():
  v = np.array([1, 0, 0])
  angle = np.array([0, 0, np.pi/2])
  assert np.allclose(rotate(angle, v), np.array([0, -1, 0]))


if __name__ == "__main__":
  test_rotacion()
  print("All tests passed")

Esta es una estructura típica de Python, donde tengo tests que prueban funciones en un módulo dado. Notemos cómo se importa el módulo rotacion desde test_rotacion.py:

from ..lib.rotacion import rotate

Al igual que con directorios, .. se refiere al directorio padre relativo al directorio actual.

Si probamos desde el directorio miproyecto/tests lo siguiente:

python test_rotacion.py

Nos encontraremos con el error:

ImportError: attempted relative import with no known parent package

Para correr el test, tenemos que ir al directorio padre del proyecto y correr el main del módulo explícitamente:

python -m miproyecto_relativo.tests.test_rotacion
All tests passed

Modificando sys.path

La forma anterior puede ser engorrosa en el caso en que se tengan muchos módulos en archivos distribuidos en una estructura de directorios complicada. Por otra parte, es posible modificar el atributo sys.path para incluir el directorio que sea de interés. En este caso, modificamos test_rotacion.py:

import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from lib.rotacion import rotate

Entonces, podemos correr desde el directorio tests:

python test_rotacion.py
All tests passed

o desde su padre como

python -m tests.test_rotacion

o

python tests/test_rotacion.py

Algunos módulos (biblioteca standard)

Los módulos pueden pensarse como bibliotecas de objetos (funciones, datos, etc) que pueden usarse según la necesidad. Hay una biblioteca standard con rutinas para muchas operaciones comunes, y además existen muchos paquetes específicos para distintas tareas. Veamos algunos ejemplos:

Módulo sys

Este módulo da acceso a variables que usa o mantiene el intérprete Python

import sys
sys.path
sys.getfilesystemencoding()
sys.getsizeof(1)
sys.getsizeof("hola")
help(sys.getsizeof)

Vemos que para utilizar las variables (path) o funciones (getsizeof) debemos referirlo anteponiendo el módulo en el cuál está definido (sys) y separado por un punto.

Cuando hacemos un programa, con definición de variables y funciones. Podemos utilizarlo como un módulo, de la misma manera que los que ya vienen definidos en la biblioteca standard o en los paquetes que instalamos.

Módulo os

El módulo os tiene utilidades para operar sobre nombres de archivos y directorios de manera segura y portable, de manera que pueda utilizarse en distintos sistemas operativos. Vamos a ver ejemplos de uso de algunas facilidades que brinda:

import os

print(os.curdir)
print(os.pardir)
print (os.getcwd())
cur = os.getcwd()
par = os.path.abspath("..")
print(cur)
print(par)
print(os.path.abspath(os.curdir))
print(os.getcwd())
print(os.path.basename(cur))
print(os.path.splitdrive(cur)) # Útil en Windows donde hay letras que identifican las unidades de disco
print(os.path.commonprefix((cur, par)))
archivo = os.path.join(par,'este' , 'otro.dat')
print (archivo)
print (os.path.split(archivo))
print (os.path.splitext(archivo))
print (os.path.exists(archivo))
print (os.path.exists(cur))

Como es aparente de estos ejemplos, se puede acceder a todos los objetos (funciones, variables) de un módulo utilizando simplemente la línea import <modulo> pero puede ser tedioso escribir todo con prefijos (como os.path.abspath) por lo que existen dos alternativas que pueden ser más convenientes. La primera corresponde a importar todas las definiciones de un módulo en forma implícita:

from os import *

Después de esta declaración usamos los objetos de la misma manera que antes pero obviando la parte de os.

path.abspath(curdir)

Esto es conveniente en algunos casos pero no suele ser una buena idea en programas largos ya que distintos módulos pueden definir el mismo nombre, y se pierde información sobre su origen. Una alternativa que es conveniente y permite mantener mejor control es importar explícitamente lo que vamos a usar:

from os import curdir, pardir, getcwd
from os.path import abspath
print(abspath(pardir))
print(abspath(curdir))
print(abspath(getcwd()))

Además podemos darle un nombre diferente al importar módulos u objetos

import os.path as path
from os import getenv as ge
help(ge)
ge('HOME')
path.realpath(curdir)

Acá hemos importado el módulo os.path (es un sub-módulo) como path y la función getenv del módulo os y la hemos renombrado ge.

curdir
[a for a in os.walk(curdir)]
help(os.walk)
import os
from os.path import join, getsize
for root, dirs, files in os.walk('./'):
    print(root, "consume ", end="")
    print(sum([getsize(join(root, name)) for name in files])/1024, end="")
    print(" kbytes en ", len(files), "non-directory files")
    if '.ipynb_checkpoints' in dirs:
        dirs.remove('.ipynb_checkpoints')  # don't visit CVS directories

Módulo glob

El módulo glob encuentra nombres de archivos (o directorios) utilizando patrones similares a los de la consola. La función más utilizada es glob.glob() Veamos algunos ejemplos de uso:

import glob
nb_clase4= glob.glob('04*.ipynb')
nb_clase4
nb_clase4.sort()
nb_clase4
nb_clases1a4 = glob.glob('0[0-4]*.ipynb')
nb_clases1a4.sort()
for f in sorted(nb_clases1a4):
  print('Clase en archivo {}'.format(f))

Módulo Argparse

Este módulo tiene lo necesario para hacer rápidamente un programa para utilizar por línea de comandos, aceptando todo tipo de argumentos y dando información sobre su uso.

import argparse
VERSION = 1.0

parser = argparse.ArgumentParser(
      description='"Mi programa que acepta argumentos por línea de comandos"')

parser.add_argument('-V', '--version', action='version',
                      version='%(prog)s version {}'.format(VERSION))

parser.add_argument('-n', '--entero', action=store, dest='n', default=1)

args = parser.parse_args()

Más información en la biblioteca standard y en Argparse en Python Module of the week

Módulo re

Este módulo provee la infraestructura para trabajar con regular expressions, es decir para encontrar expresiones que verifican “cierta forma general”. Veamos algunos conceptos básicos y casos más comunes de uso.

Búsqueda de un patrón en un texto

Empecemos con un ejemplo bastante común. Para encontrar un patrón en un texto podemos utilizar el método search()

import re
busca = 'un'
texto = 'Otra vez vamos a usar "Hola Mundo"'

match = re.search(busca, texto)

print('Encontré "{}"\nen:\n  "{}"'.format(match.re.pattern, match.string))
print('En las posiciones {} a {}'.format(match.start(), match.end()))
Encontré "un"
en:
  "Otra vez vamos a usar "Hola Mundo""
En las posiciones 29 a 31

Acá buscamos una expresión (el substring “un”). Esto es útil pero no muy diferente a utilizar los métodos de strings. Veamos como se definen los patrones.

Definición de expresiones

Vamos a buscar un patrón en un texto. Veamos cómo se definen los patrones a buscar.

  • La mayoría de los caracteres se identifican consigo mismo (si quiero encontrar “gato”, uso como patrón “gato”)

  • Hay unos pocos caracteres especiales (metacaracteres) que tienen un significado especial, estos son:

    . ^ $ * + ? { } [ ] \ | ( )
    
  • Si queremos encontrar uno de los metacaracteres, tenemos que precederlos de \. Por ejemplo si queremos encontrar un corchete usamos \[

  • Los corchetes “[” y ”]” se usan para definir una clase de caracteres, que es un conjunto de caracteres que uno quiere encontrar.

    • Los caracteres a encontrar se pueden dar individualmente. Por ejemplo [gato] encontrará cualquiera de g, a, t, o.

    • Un rango de caracteres se puede dar dando dos caracteres separados por un guión. Por ejemplo [a-z] dará cualquier letra entre “a” y “z”. Similarmente [0-5][0-9] dará cualquier número entre “00” y “59”.

    • Los metacaracteres pierden su significado especial dentro de los corchetes. Por ejemplo [.*)] encontrará cualquiera de “.”, “*“,”)“.

  • El punto . indica cualquier caracter

  • Los símbolos *, +, ? indican repetición:

    • ?: Indica 0 o 1 aparición de lo anterior

    • *: Indica 0 o más apariciones de lo anterior

    • +: Indica 1 o más apariciones de lo anterior

  • Para encontrar una cantidad determinada de caracteres, se puede agregar dicha cantidad entre llaves {}. Por ejemplo, [a-z]{3} resultará en cualquier string de exactamente tres letras minúsculas.

busca = "[a-z]+@[a-z]+\.[a-z]+" # Un patrón para buscar direcciones de email
texto = "nombre@server.com, apellido@server1.com, nombre1995@server.com, UnNombreyApellido, nombre.apellido82@servidor.com.ar, Nombre.Apellido82@servidor.com.ar".split(',')
print(texto,'\n')

for direc in texto:
  m= re.search(busca, direc)
  print('Para la línea:', direc)
  if m is None:
    print('   No encontré dirección de correo!')
  else:
    print('   Encontré la dirección de correo:', m.string)
['nombre@server.com', ' apellido@server1.com', ' nombre1995@server.com', ' UnNombreyApellido', ' nombre.apellido82@servidor.com.ar', ' Nombre.Apellido82@servidor.com.ar']

Para la línea: nombre@server.com
   Encontré la dirección de correo: nombre@server.com
Para la línea:  apellido@server1.com
   No encontré dirección de correo!
Para la línea:  nombre1995@server.com
   No encontré dirección de correo!
Para la línea:  UnNombreyApellido
   No encontré dirección de correo!
Para la línea:  nombre.apellido82@servidor.com.ar
   No encontré dirección de correo!
Para la línea:  Nombre.Apellido82@servidor.com.ar
   No encontré dirección de correo!
<>:1: SyntaxWarning: invalid escape sequence '.'
<>:1: SyntaxWarning: invalid escape sequence '.'
/var/folders/lw/b464kt25373b__wh0bxy280r0000gn/T/ipykernel_50240/497915755.py:1: SyntaxWarning: invalid escape sequence '.'
  busca = "[a-z]+@[a-z]+.[a-z]+" # Un patrón para buscar direcciones de email
  • Acá la expresión [a-z] significa todos los caracteres en el rango “a” hasta “z”.

  • [a-z]+ significa cualquier secuencia de una letra o más.

  • Los corchetes también se pueden usar en la forma [abc] y entonces encuentra cualquiera de a, b, o c.

Vemos que no encontró todas las direcciones posibles. Porque el patrón no está bien diseñado. Un poco mejor sería:

busca = "[a-zA-Z0-9.]+@[a-z.]+" # Un patrón para buscar direcciones de email

print(texto,'\n')

for direc in texto:
  m= re.search(busca, direc)
  print('Para la línea:', direc)
  if m is None:
    print('   No encontré dirección de correo:')
  else:
    print('   Encontré la dirección de correo:', m.group())
['nombre@server.com', ' apellido@server1.com', ' nombre1995@server.com', ' UnNombreyApellido', ' nombre.apellido82@servidor.com.ar', ' Nombre.Apellido82@servidor.com.ar']

Para la línea: nombre@server.com
   Encontré la dirección de correo: nombre@server.com
Para la línea:  apellido@server1.com
   Encontré la dirección de correo: apellido@server
Para la línea:  nombre1995@server.com
   Encontré la dirección de correo: nombre1995@server.com
Para la línea:  UnNombreyApellido
   No encontré dirección de correo:
Para la línea:  nombre.apellido82@servidor.com.ar
   Encontré la dirección de correo: nombre.apellido82@servidor.com.ar
Para la línea:  Nombre.Apellido82@servidor.com.ar
   Encontré la dirección de correo: Nombre.Apellido82@servidor.com.ar

Los metacaracteres no se activan dentro de clases (adentro de corchetes). En el ejemplo anterior el punto . actúa como un punto y no como un metacaracter. En este caso, la primera parte: [a-zA-Z0-9.]+ significa: “Encontrar cualquier letra minúscula, mayúscula, número o punto, una o más veces cualquiera de ellos”

Repetición de un patrón

Si queremos encontrar strings que presentan la secuencia una o más veces podemos usar findall() que devuelve todas las ocurrencias del patrón que no se superponen. Por ejemplo:

texto = 'abbaaabbbbaaaaa'

busca = 'ab'

mm =  re.findall(busca, texto)
print(mm)
print(type(mm[0]))
for m in mm:
    print('Encontré {}'.format(m))
['ab', 'ab']
<class 'str'>
Encontré ab
Encontré ab
p = re.compile('abc*')
m= p.findall('acholaboy')
print(m)
m= p.findall('acholabcoynd sabcccs slabc labdc abc')
print(m)
['ab']
['abc', 'abccc', 'abc', 'ab', 'abc']

Si va a utilizar expresiones regulares es recomendable que lea más información en la biblioteca standard, en el HOWTO, en Python Module of the week o acá.

Para practicar RegEx, ésta es una buena página.

Si efectivamente tiene que diseñar una expresión regular, esta página puede ser útil.

Ejercicios 09

  1. PARA ENTREGAR

En el ejercicio 08 se creó la clase Materia que describa una materia que se dicta en el IB. En dicha clase también se usaron las clases Persona y Estudiante. La clase Materia provee los siguientes métodos:

  • agrega_estudiante que agrega un estudiante al curso

  • agrega_docente que agrega un docente al curso

  • imprime_estudiantes que lista los estudiantes del curso

Vamos a asumir que los estudiantes son alumnos de grado, de modo tal que las carreras posibles son Ingeniería (Nuclear, Mecánica o Telecomunicaciones) o Física, o son estudiantes vocacionales. En este ejercicio le pedimos que:

  • Corrija la clase Estudiante de forma tal que sólo se puedan registrar estudiantes de grado de las carreras mencionadas, o estudiantes vocacionales.

  • Extienda la clase Materia de modo tal que se pueda registrar la nota de cada estudiante al finalizar el curso.

  • Cree la clase Admin que permita imprimir los listados de estudiantes de una materia y pueda expedir certificados de aprobación que contengan el nombre del alumno, el nombre de la materia y la nota obtenida en números y en letras. (El certificado es un archivo de texto con la información mencionada).

  • Finalmente, modifique las clases anteriores de forma tal que se puedan crear y manejar materias a través de distintos años.

Organice el código en distintos módulos dentro de un proyecto, con un main.py donde se utilicen, a modo de ejemplo, las distintas características del código solicitadas.

Para entregar el ejercicio, cree un repositorio privado en GitHub conteniendo el proyecto con el nombre ‘modelo-materias-ib’ y agregue como colaborador a cursopythonib.

  1. Un ejercicio interesante de aplicación de expresiones regulares(*) consiste en encontrar los coeficientes y las potencias de un polinomio que viene descripto como un string:

    polinomio = "5x^4 + 3x^2 - 2x + 7"
    

    (*) Este no es un ejercicio fácil, y además, para extraer la información requerida, es necesario poder capturar grupos de expresiones regulares.


.