sábado, 8 de mayo de 2010

Persistencia y herencia de objetos, con Python.

Hola. En este artículo vamos a ver las ventajas de uno de los pilares fundamentales de la programación orientada a objetos, la herencia, así como la persistencia de objetos, una forma de guardar información en disco sin necesidad de bases de datos ni ficheros estructurados. ¿Cómo? Guardando directamente el objeto en disco.

Como tantas veces se ha comentado en este blog, hay que ser ordenado, y aquí vamos a poner en práctica uno de mis pilares fundamentales de programación, a saber:

Si un objeto puede guardarse y volver a estados diferentes, podría ser interesante.

En primer lugar vamos a crear una clase base, de la cual se pueda heredar ciertos métodos que nos serán de utilidad en todas las clases que hagamos después. Es decir, la estrategia consiste en crear nuestra propia librería de clases, con ciertas clases y/o funciones si se precisara, lo más genéricas posibles, para poder reutilizar el código escrito.

Una clase base genérica heredará del objeto base de Python, esto es, object. ¿Y qué métodos podemos incluir en nuestra clase base? Pues lo métodos que nos permitan guardar ó cargar un objeto hacia ó de disco.


En este diagrama podemos ver lo que se pretende. Partimos de un objeto object que nos brinda Python. A partir de él, vamos a crear una clase base, que tendrá ciertos métodos, a saber, de para cargar y guardar los objetos que se instancien a partir de dicha clase. Y finalmente cuando queramos crear una clase, en vez de heredar de object, heredaremos de nuestra clase base.

Creación de una clase base

Una librería de clases no es ni más ni menos que un conjunto de clases y funciones genéricas a partir de las cuales heredar para poder trabajar. Esto se hace para realizar funciones específicas, ahorrándonos código y por ende, tiempo y dinero.

Nuestra librería de clases se encierra en un fichero, que denominaremos, libreria_clases.py. Aquí definiremos el siguiente código:

# -*- coding: cp1252 -*-
# Librería de clases.
# El Viaje del Navegante.
# 08-05-2010. Murcia. España

import cPickle

class clase_base(object):
# ---------------------------------------------------------------------------
# MÉTODOS DE PERSISTENCIA DE DATOS
# ---------------------------------------------------------------------------

# --------------------------------------
# METÓDOS PÚBLICOS
# --------------------------------------

def cargar_objeto(self, nombre_fichero):
'''
Devuelve un objeto guardado en el fichero cuyo nombre es nombre_fichero.
'''
return self.__cargar_objeto__(nombre_fichero)

def guardar_objeto(self, objeto, nombre_fichero):
'''
Guarda el objeto pasado como parámetro en disco con el nombre nombre_fichero.
'''
self.__guardar_objeto__(objeto, nombre_fichero)

# --------------------------------------
# METÓDOS DE SOPORTE
# --------------------------------------

def __guardar_objeto__(self, objeto, nombre_fichero):
'''
Método de persistencia de datos para guardar el estado de un objeto en disco.
La forma normal de uso es objeto.__guardar_objeto__(objeto, nombre_fichero),
en donde objeto es el objeto a guardar y nombre_fichero es el nombre del fichero
en disco que contendrá la información del objeto.
Se podría dar el caso de utilizar este método para guardar otros objetos
con el mismo nombre. Sería útil para traspasar información a partir de la
persistencia en disco.
'''
# Nombre del fichero que contendrá el objeto.
nombre_fichero = nombre_fichero
# Fichero que contendrá el objeto.
manejador_fichero = open(nombre_fichero,'w')
# Volcamos el objeto de memoria al fichero.
cPickle.dump(objeto, manejador_fichero)
# Cerramos fichero.
manejador_fichero.close()

def __cargar_objeto__(self, nombre_fichero):
'''
Método de persistencia de datos para cargar el estado de un objeto en memoria.
Se pasa como parámetro el nombre del fichero en donde está el objeto.
'''
# Nombre del fichero.
nomfich = nombre_fichero
# Abrimos fichero donde reside el objeto.
manejador_fichero = open(nombre_fichero,'r')
# Pasamos el objeto del fichero en disco a memoria.
objeto = cPickle.load(manejador_fichero)
# Cerramos el fichero.
manejador_fichero.close()
# Devolvemos el objeto.
return objeto

Lo que estamos definiendo aquí es una clase, denominada clase_base, que hereda del objeto object de Python. Vemos que tiene 4 métodos: dos de ellos ocultos (con __ al principio y al final), que llamamos de soporte, esto es, que se utilizan para realizar operaciones internas dentro de la clase; y otros dos públicos, esto es, que se pueden acceder directamente. Esta forma de trabajar es particular mía, con dos métodos hubiera bastado. Sin embargo, si se escribe un poco más de código la legibilidad aumenta.

¿Qué realiza esta clase? Esta clase implementa los métodos para guardar y cargar, una vez que se instancie, el objeto creado en disco a partir de dicha instanciación. Para ello utilizamos el módulo de persistencia de datos cPickle. En Python existen varios módulos relacionados con la persistencia de datos como pickle, copy_reg, shelve, marshal, anydbm, whichdb, dbm, gdbm, dbhash, bsbdb, dumbdbm, sqlite3, además de cPickle.

Creación de una clase a partir de nuestra clase base

Para ver el potencial de nuestra clase base vamos a realizar un sencillo ejemplo, que el lector entenderá sin ningún problema. Creamos una clase que defina un ser humano (evidentemente faltarían muchos atributos, que he obviado). Dicha clase, en vez de heredarla de un objeto object se va a heredar de la clase clase_base. Esta clase se define en un fichero diferente, denominado ejemplo_ser_humano.py. Contiene el siguiente código:

# -*- coding: cp1252 -*-
# Ejemplo de herencia y persistencia de datos.

from libreria_clases import clase_base

class ser_humano(clase_base):
def __init__(self,color_ojos, color_pelo,altura,peso):
'''
Definición de un ser humano. Parámetros:
color de ojos, coloe de pelo, altura, peso.
'''
self.__color_ojos__ = color_ojos
self.__color_pelo__ = color_pelo
self.__altura__ = altura
self.__peso__ = peso

def devolver_datos(self):
'''
Devuelve diccionario de datos sobre el ser humano.
'''
lista = [("color_ojos",self.__color_ojos__),\
("color_pelo",self.__color_pelo__),\
("altura",self.__altura__),\
("peso",self.__peso__)]
return dict(lista)


Lo primero que vemos es que importamos la clase clase_base. Esto es necesario debido a que estamos definiendo la clase ser_humano a partir de la clase clase_base.

class ser_humano(clase_base):

Esta clase es trivial, esto es, contiene un __init__ en donde se definen los atributos ocultos y un método que devuelve un diccionario con los valores actuales de los atributos que definen al ser humano (se le recuerda al lector que he abreviado los atributos para no alargar en exceso el ejemplo).

Utilización de clases con nuevos métodos incorporados

Bien, ha llegado el momento que todos esperábamos. Tenemos una clase, ser_humano, que contiene, no solamente el método que devuelve el diccionario; además tiene los métodos guardar y cargar que heredó de la clase base de la que hereda. Si nos vamos a cualquier editor con completitud de código, como IDLE, podremos verlo:

Para ver cómo guardar un objeto creado podemos crear un nuevo fichero, llamado, por ejemplo, humano.py, que contendrá lo siguiente:

# Ejemplo de utilización de cPickle.
# El Viaje del Navegante

from ejemplo_ser_humano import ser_humano

humano = ser_humano("azul","marron",1.80,65)
humano.guardar_objeto(humano,"datos_humano.obj")

Si nos vamos al directorio en donde está nuestro fichero de código fuente veremos que se nos ha creado un nuevo archivo, llamado datos_humano.obj (la extensión obj no es obligatoria, yo la pongo por convenio). Si editamos su contenido obtenemos lo siguiente:

Vemos que el fichero contiene, además de cierta información propia, nuestros datos introducidos. Para ver cómo cargar dicho objeto hagamos alguna modificación al código de humano.py, tal que así:

# Ejemplo de utilización de cPickle.
# El Viaje del Navegante

from ejemplo_ser_humano import ser_humano

#humano = ser_humano("azul","marron",1.80,65)
humano = ser_humano("-","-",0,0)
#humano.guardar_objeto(humano,"datos_humano.obj")
humano = humano.cargar_objeto("datos_humano.obj")
print humano.devolver_datos()

Como el lector puede apreciar, se han comentado las líneas de creación de un humano específico, así como la utilización del método para guardar el objeto. Hemos creado un objeto humano con valores diferentes y nos disponemos a cargar el objeto, para a continuación imprimir el diccionario de datos con los valores de los atributos en disco. Como no podría ser de otra manera el resultado es:

{'peso': 65, 'altura': 1.8, 'color_pelo': 'marron', 'color_ojos': 'azul'}
Con esto queda demostrado que la persistencia de datos funciona.

Un ejemplo más serio: Gestión de personal

Puesto que ya hemos visto como funciona la persistencia de datos y la filosofía a la hora de trabajar con librerías de clases (o al menos como debería de ser), nos disponemos a crear una aplicación consistente con dicha técnica. En primer lugar dicha aplicación se hará en modo texto, sin implementación en entorno gráfico. Esto lo hago así para que el lector pueda apreciar que hay que intentar ser lo más genérico posible a la hora de desarrollar una aplicación. Es decir, si conseguimos crear una aplicación que funcione, indenpendientemente de la plataforma gráfica y de la plataforma del sistema operativo (con Python es muy fácil), tendremos mucho ganado, ya que pasar de wxPython a GTK, por ejemplo, será cuestión de minutos (siempre y cuando la interfaz gráfica ya esté hecha, claro).

En las aplicaciones que desarrollemos se intentará, por todos los medios, que haya una independencia de la plataforma gráfica en la que trabajemos, en lo máximo que sea posible.
¿En qué consiste nuestra aplicación? Nuestro programa gestionará una base de datos de personas. La funcionalidad será la de dar de alta, baja y búsqueda de personas. Además, se podrán guardar las modificaciones en disco. En consecuencia, se podrá recuperar información almacenada anteriormente.

Nuestra aplicación se basará en la clase clase_base. Por tanto tomaremos en cuenta el fichero, tal como está implementado anteriormente, libreria_clases.py.

En un segundo fichero, llamado, clase_persona.py, tendremos la definición de las clases:

persona: Clase, que hereda de object, que define a una persona. Contiene todos los métodos necesarios para gestionar su información.

total_personas: Clase, que hereda de clase_base, que define a un conjunto de objetos instanciados a partir de la clase persona. Esta clase contiene una lista de objetos, y los métodos para guardar y cargar objetos, heredados de la clase_base. Se utiliza esta clase para guardar una lista de objetos persona, es decir, el conjunto, ya que de lo contrario se tendria que crear un fichero para cada persona (a la hora de guardar), y no es muy operativo.

Por último, en un tercer fichero, llamado aplicacion_texto_personas.py, tendremos las funciones necesarias para crear un menú amigable de texto, así como aquellas funciones en las que se utilicen las clases (los objetos instanciados a partir de ellas) definidas en los anteriores ficheros.

clase_persona.py: Definiendo las clases fundamentales

A continuación se presenta el contenido del código de clase_persona.py, que contiene las clases fundamentales de nuestra aplicación:

# -*- coding: cp1252 -*-
# Clase genérica persona.
# El Viaje del Navegante.
# 08-05-2010. Murcia. España.

from libreria_clases import clase_base
import re

class persona(object):
'''
Clase persona. Identifica a una entidad persona.
'''
def __init__(self):
'''
Declaramos aquí todos los atributos de la persona.
'''
self.__nombre__ = ''
self.__apellido1__ = ''
self.__apellido2__ = ''
self.__nif__ = ''
self.__direccion__ = ''
self.__poblacion__ = ''
self.__codigo_postal__ = ''
self.__provincia__ = ''
self.__pais__ = ''
self.__telefono1__ = ''
self.__telefono2__ = ''
self.__email__ = ''
self.__web__ = ''

def dato(self, opcion):
'''
Devuelve un atributo del objeto, especificado según el parámetro "opcion".
"opcion" puede ser:
nombre: Nombre
apellido1: Primer apellido
apellido2: Segundo apellido
nif: NIF
direccion: Dirección
poblacion: Población
cp: Código postal
provincia: Provincia
pais: País
tlfno1: Primer número de teléfono
tlfno2: Segundo número de teléfono
email: Correo electrónico
web: Sitio web ó blog personal
todo: Devuelve una lista con todos los atributos.
'''
if opcion == 'nombre': return self.__nombre__
if opcion == 'apellido1': return self.__apellido1__
if opcion == 'apellido2': return self.__apellido2__
if opcion == 'nif': return self.__nif__
if opcion == 'direccion': return self.__direccion__
if opcion == 'poblacion': return self.__poblacion__
if opcion == 'cp': return self.__codigo_postal__
if opcion == 'provincia': return self.__provincia__
if opcion == 'pais': return self.__pais__
if opcion == 'tlfno1': return self.__telefono1__
if opcion == 'tlfno2': return self.__telefono2__
if opcion == 'email': return self.__email__
if opcion == 'web': return self.__web__
if opcion == 'todo': return [self.__nombre__,self.__apellido1__,self.__apellido2__,\
self.__nif__,self.__direccion__,self.__poblacion__,\
self.__codigo_postal__,self.__provincia__,self.__pais__,\
self.__telefono1__,self.__telefono2__,self.__email__,\
self.__web__]

def buscar(self, opcion, expresion_regular):
'''
Función para buscar una expresión regular en el campo dado por el
parámetro "opcion".
"opcion" puede ser:
nombre: Nombre
apellido1: Primer apellido
apellido2: Segundo apellido
nif: NIF
direccion: Dirección
poblacion: Población
cp: Código postal
provincia: Provincia
pais: País
tlfno1: Primer número de teléfono
tlfno2: Segundo número de teléfono
email: Correo electrónico
web: Sitio web ó blog personal
Devuelve cierto si se ha encontrado una corcondancia con la expresión regular
ó falso en caso contrario.
'''
# No se diferencia entre mayúsculas y minúsculas.
re.IGNORECASE
# Se ignoran espacios en blanco.
re.VERBOSE
# Buscamos la cadena en el campo indicado.
try:
if re.search(expresion_regular, self.dato(opcion)) is None:
# No se ha encontrado concordancia. Se devuelve Falso.
return False
else:
# Hay concordancia, ya que re.search devuelve un objeto.
# Se devuelve cierto.
return True
except:
return False

def modificar(self, opcion, valor = None):
'''
Asigna el valor "valor" al atributo "opcion".Si no se especifica "valor", por defecto
se asigna None.
"opcion" puede ser:
nombre: Nombre
apellido1: Primer apellido
apellido2: Segundo apellido
nif: NIF
direccion: Dirección
poblacion: Población
cp: Código postal
provincia: Provincia
pais: País
tlfno1: Primer número de teléfono
tlfno2: Segundo número de teléfono
email: Correo electrónico
web: Sitio web ó blog personal
'''
if opcion == 'nombre': self.__nombre__ = valor
if opcion == 'apellido1': self.__apellido1__ = valor
if opcion == 'apellido2': self.__apellido2__ = valor
if opcion == 'nif': self.__nif__ = valor
if opcion == 'direccion': self.__direccion__ = valor
if opcion == 'poblacion': self.__poblacion__ = valor
if opcion == 'cp': self.__codigo_postal__ = valor
if opcion == 'provincia': self.__provincia__ = valor
if opcion == 'pais': self.__pais__ = valor
if opcion == 'tlfno1': self.__telefono1__ = valor
if opcion == 'tlfno2': self.__telefono2__ = valor
if opcion == 'email': self.__email__ = valor
if opcion == 'web': self.__web__ = valor

def cabecera(self):
'''
Devuelve una lista de cabecera, relacionando campo y texto asociado.
'''
cabecera = [("nombre","Nombre: "),("apellido1","1er. Apellido: "),\
("apellido2","2do. Apellido: "),("nif","DNI/NIF: "),\
("direccion","Dirección: "),("poblacion","Población: "),\
("cp","Código postal: "),("provincia","Provincia: "),\
("pais","País: "),("tlfno1","Teléfono 1: "),\
("tlfno2","Teléfono 2: "),("email","Correo electrónico: "),\
("web","Sitio web / blog: ")]
# Devuelve cabecera.
return cabecera

class total_personas(clase_base):
'''
Clase que define una lista de personas. Se utiliza para serializar el
conjunto de clientes en un objeto.
'''
def __init__(self, lista_personas):
self.__lista_personas__ = lista_personas
self.__persona__ = 0

def devolver_persona(self, movimiento = "actual"):
'''
Método que devuelve una persona, a partir del valor
de "movimiento", que puede tomar los siguientes valores:
actual: Devuelve la persona actual.
siguiente: Devuelve la persona siguiente, y se posiciona en ella.
anterior: Devuelve la persona anterior, y se posiciona en ella.
ultimo: Devuelve la última persona y se posiciona en ella.
primero: Devuelve la primera persona y se posiciona en ella.
'''
if movimiento == "siguiente": self.__persona__ += 1
if movimiento == "anterior": self.__persona__ -= 1
if movimiento == "ultimo": self.__persona__ = len(self.__lista_personas__)
if movimiento == "primero": self.__persona__ = 1
# Devolvemos a la persona requerida.
return self.__lista_personas__[self.__persona__]

def devolver_lista_personas(self):
'''
Devuelve la lista de personas.
'''
return self.__lista_personas__

Como se ha comentado se diseñan dos clases, la que define a la persona y la que define al conjunto de personas. Decir que en la búsqueda de objetos persona se utiliza el módulo de expresiones regulares re, más concretamente con el método search. El código está lo suficientemente comentado como para que no haya problemas en su comprensión.

Por último, vamos a ver el código que maneja las clases definidas.

Juntando todo: la aplicación

Finalmente tenemos el código de aplicacion_texto_personas.py, en donde están las funciones necesarias para ejecutar la aplicación.

# -*- coding: cp1252 -*-
import os

from clase_persona import persona
from clase_persona import total_personas

# Menú de opciones.
def menu():
os.system('cls')
print '''

*** GESTIÓN DE PERSONAS ***

Elige una de las siguientes opciones:

1) Alta de personas.
2) Baja de personas.
3) Modificación de persona.
4) Búsqueda.
5) Impresión de persona.
6) Salvar.
7) Cargar.
0) Salir.

Elige opción:
'''
opcion = raw_input("==> ")
return opcion

def pedir_parametros_busqueda(cabecera):
'''
Función para pedir parámetros de búsqueda.
Devuelve una lista de parámetros.
'''
lista_parametros = []
for i in cabecera:
lista_parametros.append((i[0],raw_input(i[1])))
return lista_parametros

def modificar_persona(persona, opcion, lista_personas):
'''
Función para modificar a una persona, según el valor de opción.
Si opcion = 0 se pide la inserción de datos, normal.
Si opcion = 1 se pide, mediante confirmacion, la modificación de datos.
'''
cabecera = persona.cabecera()
for i in cabecera:
seguir = True
while seguir:
if opcion == 0: persona.modificar(i[0], raw_input(i[1]))
if opcion == 1:
j = raw_input("¿Modificar " + i[0] + " (S/N)?")
if j in ("S","s"):
persona.modificar(i[0], raw_input("("+ persona.dato(i[0])+") " + i[1]))
else:
# No se modifica nada. Se continúa.
# Hacemos break para salir del while.
break
# Aquí incluimos las restricciones de inserción de datos.
# El NIF nunca puede estar vacío.
if len(str(persona.dato("nif"))) == 0 and i[0] == 'nif':
print "Hay que incluir obligatioramente el NIF."
else:
seguir = False
# El NIF no puede estar repetido.
if i[0] == 'nif' and not seguir:
contador = 0
for k in lista_personas:
if k.dato("nif") == persona.dato("nif"): contador += 1
if (contador == 1 and opcion == 0) or (contador > 1 and opcion == 1):
print "Hay una persona con un mismo NIF ", str(persona.dato("nif")).strip()
seguir = True
else:
seguir = False
# Devolvemos a la persona.
return persona

def aplicacion():
#Nombre del fichero de disco en donde se guarda la información de las personas.
nombre_fichero = "bd_personas.obj"
# Lista de objetos clientes.
lista_personas = []
opcion = ""
while opcion != "0":
opcion = menu()
if opcion == "7":
# Cargar información del disco.
if len(lista_personas) != 0:
print "¡Atención! Hay datos en memoria. Si continúas borrarás"+ \
"lo que hayas realizado has ahora."
aux = raw_input("¿Cargar información de disco (S/N)?")
if aux not in ("S","s"): return
personas = total_personas([])
try:
personas = personas.cargar_objeto(nombre_fichero)
aux = personas.devolver_lista_personas()
if len(aux) != 0: lista_personas = aux
except:
# Error en la carga del fichero.
print "No he podido cargar el fichero ", nombre_fichero

if opcion == "6":
# Guardar información en disco.
if len(lista_personas) != 0:
personas_aux = total_personas(lista_personas)
personas_aux.guardar_objeto(personas_aux,nombre_fichero)

if opcion == "1":
# Alta de persona.
persona_nueva = persona()
# Info.
print "Introduce valores para crear una nueva persona:"
persona_nueva = modificar_persona(persona_nueva,0,lista_personas)
# Añadimos persona a la lista de personas.
lista_personas.append(persona_nueva)
if opcion == "5":
# Listado de todas las personas.
for i in lista_personas:
print i.dato('todo')

if opcion == "2" or opcion == "3":
# Baja ó modificación de una persona, a partir de su NIF.
# Info.
if opcion == "2": aux = " dar de baja "
if opcion == "3": aux = " modificar los datos de "
print "Para"+aux+"una persona, introduzca su NIF."
nif = raw_input("NIF: ")
if len(str(nif)) != 0:
for i in lista_personas:
if i.dato("nif") == nif:
if opcion == "2": lista_personas.remove(i)
if opcion == "3": modificar_persona(i,1,lista_personas)
# Info.
if opcion == "2": aux = "eliminó"
if opcion == "3": aux = "modificó"
print "Se "+aux+" la persona con NIF ", str(nif)
# Salimos.
break

if opcion == "4":
# Info.
print """
Para buscar hay que definir parámetros de búsqueda. Si se quiere
obviar un campo simplemente pulse Enter. Defina parámetros de búsqueda:
"""
pedir_datos = True
# Inicializamos testigo de búsqueda.
encontrado = True
# Búsqueda de personas en la lista de personas.
for persona_actual in lista_personas:
if pedir_datos:
# Pedimos inserción de información.
lista_parametros_busqueda = pedir_parametros_busqueda(persona_actual.cabecera())
pedir_datos = False
# Para cada persona, buscamos en la lista de expresiones
encontrado = True
for i in lista_parametros_busqueda:
# Campo a buscar.
opcion = i[0]
# Expresión regular asociada al parámetro.
expresion_regular = i[1]
# Si la expresión regular no contiene nada, seguimos.
if len(str(expresion_regular)) == 0: continue
# Evaluamos la expresión para la persona actual.
if persona_actual.buscar(opcion, expresion_regular):
# La expresión regular coincide. Seguimos.
continue
else:
# No hay coincidencia. La persona no coincide
# con los parámetros de búsqueda especificados.
encontrado = False
break
if encontrado:
# Aquí se podría guardar el resultado de la búsqueda.
# En nuestro caso se imprime el resultado.
print persona_actual.dato('todo')

# Ejecutamos la aplicación.
aplicacion()


Como antes, el código está lo suficientemente comentado como para que no haya excesivos impedimentos para su entendimiento.

Funcionamiento de la aplicación

Y para terminar, unas capturas de pantalla de cómo debería de funcionar esta prototipo de aplicación de gestión de personal. Decir que en la impresión no me he esmerado nada, solo imprimo la lista de personas. Dejo al lector su desarrollo.

La pantalla principal.

Alta de algunas personas.

Impresión.

Búsqueda.

Modificación.

Y por supuesto la eliminación de una persona, así como guardar y cargar lista de personas.

Esta aplicación es un prototipo, en modo texto, por lo que habría que pulir la interfaz del usuario. Darse cuenta que hemos utilizado la clase clase_base para guardar nuestro objeto en disco, de manera que nos olvidamos de bases de datos y ficheros con lógica estructurada.

Un Bug!!!!

Me he dado cuenta que si introduces datos de una determinada forma la aplicación se vuelve inconsistente. Esto es, es obligatorio el NIF de una persona, ya que lo trato como clave de identificación. Sin embargo, pudiera darse el caso de modificar una persona y no asignarle NIF.

Véase aquí:



CONCLUSIONES

En este artículo hemos visto como implementar una clase que, al instanciarse, se puede guardar, y al recuperarse, puede cambiar de estado. Hemos visto las ventajas de trabajar con clases bases y por ende, de la necesidad de crear una librería de clases para poder trabajar, ya que de esta manera podemos reutilizar código ya escrito. Es más, si necesitamos modificar alguna clase base, se podría versionar, siempre que se pueda.

Se ha trabajado con cPickle, uno de los módulos que tiene Python para la serialización de objetos, esto es, la persistencia de datos.

Espero que os pueda servir de ayuda mi forma de ver la construcción de aplicaciones.

Un cordial saludo.

2 comentarios: