El siguiente post contiene un ejemplo de desarrollo de un pequeño proyecto con Python. Este proyecto hace uso de algunos de los elementos de la orientación a objetos en Python, asi como de algunas recomendaciones en el desarrollo con Python.
Que cocinamos
Un pequeño proyecto con Python que incluye lo siguiente:
- Clases
- Módulos
- Paquetes
- Ficheros de log
- Tests
- Documentación autogenerada
El proyecto se puede descargar integro desde https://github.com/luisalbertogh/oopython.git.
Ingredientes
Los requisitos para este proyecto son los siguientes:
- Python 3.x
- Sphinx [http://www.sphinx-doc.org/en/stable/install.html]
- Your favourite editor
Paso a paso
Creamos una estrcutura para el proyecto con el siguiente diseño:
<proyecto>
|__ <config> [ficheros de configuración]
|__ <doc> [documentación]
|__ <proyecto> [código fuente]
|__ <test> [tests de unidad e integración]
Vease el conteido del proyecto a modo de ejemplo.
Código fuente
El código se estructura de la siguiente manera (de más básico a más complejo):
1. Creamos un módulo base que contiene una serie de clases que incluyen nuestro modelo y nuestra lógica de negocio [oopython\mypackage\module01.py]. Aquí encontramos:
Superclases
1 2 3 4 5 6 7 8 |
#Sportman superclass with init method. class Sportbeing(metaclass=abc.ABCMeta): """ Init method. """ def __init__(self, gender, nationality): self.gender = gender; self.nationality = nationality; |
Clases con diferente tipos de métodos (estáticos, constructores (__init__), sobrecargados (métodos especiales que empiezan con __. etc))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#Subclass extends from superclass. class Cyclist(Sportbeing): """ Init method. """ def __init__(self, gender, nationality, name): # Invoke superclass init super().__init__(gender, nationality); self.name = name; """ Static mehod to create instances. """ @staticmethod def createInstance(): return Cyclist('male','Spanish','Valverde'); """ Set name. """ def setName(self, team): self.name = name; """ Print instance details (used in print). """ def __str__(self): return self.gender + '; ' + self.nationality + '; ' + self.name; |
Lazy properties, setters y getters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
""" Lazy property, created when invoked. """ @property def director(self): return self._director; """ Setter and deleter for riders. """ @director.setter def director(self, director): self._director = director; @director.deleter def director(self): self._director = None; |
Clases estratégicas (sin estado, sólo lógica)
1 2 3 4 5 6 7 |
# Strategy class (stateless, no data, only logic). class StageRules: def win(self, rider1, rider2): return True; def lose(self, rider1, rider2): return False; |
2. Incluimos este módulo en un paquete. Esto no es necesario en Python pero puede ser una buena recomendación para modular un poco más y aportar un nivel adicional de granularidad al código desarrollado. Los paquetes estarían compuestos por módulos y estos se podrían usar desde otros módulos y/o paquetes.
Para esto, simplemente creamos el directorio oopython\mypackage, incluimos el módulo recien creado dentro y añadimos un fichero __init__.py. Adicionalmente podemos incluir en el fichero __init__.py otros módulos o paquetes externos o indicar que módulos internos queremos habilitar desde el paquete, de tal manera que sea más sencillo invocarlos cuando se usen desde otros módulos externos. Si añadimos lo siguiente:
1 2 |
""" mypackage """ from mypackage.module01 import *; |
Podremos usar el contenido del paquete mypackage desde otros módulos externos simplemente haciendo:
1 |
from mypackage import * |
De otra manera tendriamos que especificar el módulo, etc.
3. Creamos un nuevo módulo [oopython\oopython.py] que haga uso del paquete anteriormente creado.
1 2 3 4 5 6 7 |
# Import classes from module from mypackage import *; ... mov = Team('Movistar', Cyclist('male','Spanish','Valverde'), Cyclist('male','Colombian','Quintana'), Cyclist('male','Basque','Izagirre')); logger.info(mov); logger.info('Num. de corredores: {0}'.format(mov.totalriders)); ... |
Ficheros de log
Para disponer de una serie de loggers que permitan crear ficheros de log e imprimir mensajes por pantalla, usamos paquetes estandar de logging de Python y YAML como lenguaje para los ficheros de configuración de los mismos.
En caso de que YAML no esté instalado, usar:
1 |
$ pip install pyyaml |
En el módulo principal incluimos el siguiente código:
1 2 3 4 5 6 7 8 9 10 |
# YAML & logging import yaml; import logging; import logging.config; ... # Init logger logdict = yaml.load(open('config/log_config.yaml', 'r')); logging.config.dictConfig(logdict); logger = logging.getLogger('MyLogger'); ... |
El fichero de configuracion de los logs se encuentra en la ruta config/log_config.yaml. Contiene los valores necesarios para crear differentes tipos de loggers, configurar salidas por consola, formato de los logs, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... handlers: console: class: logging.StreamHandler stream: ext://sys.stderr formatter: basic mylogger: class: logging.handlers.TimedRotatingFileHandler filename: mylog.log encoding: utf-8 formatter: basic when: D # S - seconds, M - minutes, H - hours, D - Days, W0-W6 - weekday, midnight interval: 1 backupCount: 5 ... |
Tests
Tal y como ocurre en otros lenguajes de programación, aqui también podemos crear tests de unidad y de integración que se ejecuten de forma automática dentro de un proceso de integración continua.
En un directorio separado dentro del proyecto llamado test, creamos diferentes ficheros (módulos) con diferentes grupos de tests. Cada módulo podría ser considerado como un test suite diferente, por ejemplo.
Dentro de cada módulo incluimos diferentes tests unitarios usando el módulo de Python para implementar tests de unidad:
1 |
import unittest; |
Para que los tests puedan hacer uso de los módulos y paquetes internos del proyecto, se setea la ruta principal del proyecto relativa al test:
1 2 3 4 5 6 7 |
""" Apend module path so tests can used project modules. """ import sys; import os; sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),"..")) + "\oopython"); from mypackage import *; |
Los tests unitarios son métodos del siguiente TestCase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Test case class class TestOopython(unittest.TestCase): """ Test case. """ #Set up test case def setUp(self): self.testteam = Team('Test', Cyclist('male','Spanish','Valverde'), Cyclist('male','Colombian','Quintana'), Cyclist('male','Basque','Izagirre')); # Test number of raiders def test_team_members(self): self.assertEqual(3, self.testteam.totalriders); # Test included raiders def test_raiders(self): self.assertEqual(Cyclist('male','Spanish','Valverde'), self.testteam[0]); self.assertEqual(Cyclist('male','Colombian','Quintana'), self.testteam[1]); self.assertEqual(Cyclist('male','Basque','Izagirre'), self.testteam[2]); |
Los test unitarios se cargan en un único test suite:
1 2 3 4 5 6 |
# Test suite with pervious test cases def testsuite(): s = unittest.TestSuite(); load_from = unittest.defaultTestLoader.loadTestsFromTestCase; s.addTests(load_from(TestOopython)); return s; |
Y se ejcutan:
1 2 3 4 5 6 7 |
# Run test suite if __name__ == "__main__": """ Run test suite """ tr = unittest.TextTestRunner(); tr.run(testsuite()); |
Una manera sencilla de lanzar todos (o los seleccionados) tests es usando el siguiente comando:
1 |
$ python -m unittest discover test |
Se indica el uso del módulo de testeo de Python y se le indica que ejecute todos los tests unitarios que encuentre en el directorio test correspondiente. También se pueden lanzar como un scrupt de Python sin más.
Generación automática de documentación
La generación automática está basada en el uso de la herramienta Sphinx, que es una de las más extendidas. Los pasos para ejecutarla son basicamente los siguientes:
Instalar Sphinx
http://www.sphinx-doc.org/en/stable/install.html
Ejecutar Sphinx quickstart
Se recomienda seguir este tutorial para concer las opciones más relevantes de este comando. Es importante activar la generación automática de documentación si queremos que la herramienta convierta los comentarios incluidos en el código en Python como texto dentro de la documentación.
http://www.sphinx-doc.org/en/stable/tutorial.html
1 |
$ sphinx-quickstart |
Revisar valores en conf.py
Antes de lanzar la generación automática de documentación conviene revisar el fichero de configuración autogenerado por el paso anterior. Éste se encuentra en oopython\doc\source\conf.py.
Una de las modificaciones que tenemos que aplicar al fichero recien creado es indicar la ruta en la que se encuentra el código de Python para que los pasos siguientes puedan autogenerar documentación:
1 |
sys.path.insert(0, os.path.abspath('../../oopython')) |
De esta manera se añade a la ruta en la que Python busca los módulos y paquetes la ruta que contiene nuestro código.
Sphinx apidoc
Nos permite de forma automática generar los ficheros RST que se usaran para dar formato a la páginas de la documentación que se incluirá asi como su contenido. Normalmente se creara un fichero RST por módulo/paquete y se añadirá información sobre las clases y miembros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Submodules ---------- mypackage.module01 module ------------------------- .. automodule:: mypackage.module01 :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: mypackage :members: :undoc-members: :show-inheritance: |
Más información aqui: http://scriptsonscripts.blogspot.com.es/2012/09/quick-sphinx-documentation-for-python.html
El comando de ejecución sería el siguiente. doc/source sería el directorio en el que se generarán los RST y oopython el directorio que contiene el código fuente en Python.
1 |
$ sphinx-apidoc -F -o doc/source oopython |
Crear la documentación
Se lanza el siguiente comando:
1 |
$ sphinx-build -b html doc/source/ doc/build/ |
Y se genera el formato de salida deseado, en este caso en HTML:
1 2 |
$ cd doc $ make html |