viernes, 25 de junio de 2010

Para aprender hay que equivocarse...

El único hombre que no se equivoca es el que nunca hace nada.


Johann Wolfgang Goethe

lunes, 14 de junio de 2010

Gestión de ficheros de configuración de aplicaciones, en Python

Hola. En este post vamos a ver cómo gestionar ficheros de configuración de aplicaciones.

Un fichero profile, también llamado fichero de perfil, fichero de configuración ó de inicialización es un archivo de texto plano, normalmente con extensión .INI (aunque no es indispensable), consistente en una serie de etiquetas de sección, las cuales se encierran entre corchetes ([]), y claves, las cuales son seguidas por un signo igual (=) y un valor. Cambiando los valores asignados a las claves se puede especificar una configuración personalizada (custom) para cada instalación de una aplicación.

Cuando se quiere crear un fichero de configuración, se tiene que seleccionar el nombre de la sección y clave y determinar los valores que se usarán para dicha clave.

Por ejemplo, un fichero profile podría contener información sobre un usuario de una aplicación. En el siguiente ejemplo, la información de un usuario es el nombre de la sección y los otros valores son las claves.

[INFO_USUARIO]
NOMBRE=ANGEL LUIS GARCIA GARCIA
PROFESION=PROGRAMADOR INFORMÁTICO
PAÍS=ESPAÑA
PASSWORD=123456

Dentro de una sección no puede haber nombres de claves repetidas (aunque sí se pueden repetir los valores de las claves). Por ejemplo, el siguiente fichero profile es incorrecto:

[INFO_USUARIO]
NOMBRE=ANGEL LUIS GARCIA GARCIA
PROFESION=PROGRAMADOR INFORMÁTICO
PROFESION=ANALISTA DE SISTEMAS

Por ende, no puede haber nombres de secciones repetidas. El siguiente ejemplo, es incorrecto:

[INFO_USUARIO]
NOMBRE=ANGEL LUIS GARCIA GARCIA
PROFESION=PROGRAMADOR INFORMÁTICO

[INFO_USUARIO]
PAÍS=ESPAÑA

Un fichero profile correcto sería el siguiente:

[INFO_USUARIO]
NOMBRE=ANGEL LUIS GARCIA GARCIA
PROFESION=PROGRAMADOR INFORMÁTICO
PROFESION2=ANALISTA DE SISTEMAS
PAÍS=ESPAÑA

Para poder manipular ficheros profiles (también llamados .INI), he creado una clase denominada profilePython.En esta clase se pueden utilizar los siguientes 3 métodos:

profile: Método que devuelve el valor de una clave en una sección. Su sintáxis es la siguiente:

profile(nombre_seccion, nombre_clave)
profile(nombre_seccion, nombre_clave, valor_por_defecto)


donde el parámetro nombre_seccion es el nombre de la sección; el parámetro nombre_clave es el nombre de la clave, y el parámetro valor_por_defecto, que es opcional, es el valor que devuelve el método si no se ha encontrado la clave dentro de la sección especificada.

set_profile: Método con el que se pueden crear secciones, claves y asignar valores a claves. Su sintaxis es la siguiente:

set_profile(nombre_seccion): Crea una sección con nombre nombre_seccion.
set_profile(nombre_seccion,nombre_clave): Crea una clave con nombre nombre_clave dentro de la sección con nombre nombre_seccion.
set_profile(nombre_seccion,nombre_clave,nombre_valor): Crea ó asigna si ya existe, el valor nombre_valor a la clave nombre_clave dentro de la sección nombre_seccion.

save_profile: Método para guardar la estructura de secciones y claves con sus respectivos valores en un fichero profile, cuyo nombre se define a la hora de la instanciación de la clase. Su sintaxis es la siguiente:

save_profile(): Guarda las secciones con sus claves y correspondientes valores en un fichero profile, en disco. Por defecto, si hay un fichero profile con el mismo nombre, se crea una copia de seguridad del mismo.
save_profile(False): Si se parametriza con valor False, no se crea copia de seguridad del fichero profile en disco.

¿Cómo se crea un fichero profile?

Mediante la instanciación de la clase profilePython. Así, si queremos que nuestro fichero de inicialización se llame config.ini, tendremos que es tan sencillo como escribir:

fich_ini = profilePython('config.ini')

La instanciación crea si no existe el fichero config.ini. Si el fichero existe, el objeto se guarda toda la estructura de secciones y claves con sus respectivos valores que se encuentra en dicho fichero. Si no se incluye ruta absoluta se crea en la misma ruta donde esté el fichero que contiene la aplicación Python.

Crear un fichero de configuración de una aplicación

Vamos a crear un fichero de configuración, con 2 secciones, una para los datos personales del usuario y otra para una personalización de la aplicación. El fichero de configuración se llamará config_apli.ini. Suponemos que la clase profilePython está en el fichero class_profiles.py. Lo primero de todo, importar la clase profilePython:

from class_profiles import profilePython

Instanciamos la clase:

config = profilePython('config_apli.ini')

Creamos una sección llamada DATOS_PERSONALES, con 4 claves:

config.set_profile('DATOS_PERSONALES','EMPRESA','MI EMPRESA SA') config.set_profile('DATOS_PERSONALES','CIF','A123456789') config.set_profile('DATOS_PERSONALES','TELEFONO','968000000') config.set_profile('DATOS_PERSONALES','DIRECCION','CALLE MAR MENOR, 30 MURCIA')

Creamos una sección llamada CUSTOM_APLIC, con 2 claves:

config.set_profile('CUSTOM_APLIC','ACTIVAR_CONTA','SI') config.set_profile('CUSTOM_APLIC','IVA',18)

Finalmente, guardamos esta estructura en el fichero, indicándole que no queremos que haga copia de seguridad, mediante:

config.save_profile(False)

El fichero resultante es el siguiente:


Una vez cargada nuestra aplicación necesitamos recuperar la información de configuración. Para ello tan sencillo como hacer lo siguiente:

from class_profiles import profilePython

config = profilePython('config_apli.ini')

Obtenemos los valores de los datos personales específicos del cliente que utiliza la aplicación. Por ejemplo:

empresa = config.profile('DATOS_PERSONALES','EMPRESA')

print empresa



Pudiera ser que utilizásemos para diferentes versiones de nuestra aplicación un mismo fichero de configuración, con el consiguiente problema que hay claves que se utilizan en una versión y no en otra. No hay problema, podemos utilizar el valor por defecto del método profile. Así, si queremos buscar una clave que no sabemos si está o no el fichero, podemos decirle que cargue un valor por defecto. Por ejemplo:

web = config.profile('DATOS_PERSONALES','WEB','SIN PÁGINA WEB')

print web



Para poder modificar un valor de una clave dentro de una sección, solamente habría que utilizar el método set_profile. Imaginemos que queremos cambiar el valor de la clave IVA de la sección CUSTOM_APLIC. Tan sencillo como:

from class_profiles import profilePython

config = profilePython('config_apli.ini')

config.set_profile('CUSTOM_APLIC','IVA',16)


config.save_profile(False)


Imaginemos ahora que queremos cambiar el nombre de la empresa, y queremos que se haga una copia del fichero profile original.

Tan sencillo como:

from class_profiles import profilePython

config = profilePython('config_apli.ini')


config.set_profile('DATOS_PERSONALES','EMPRESA','MURTHON SL')


config.save_profile()




Como vemos en los ficheros seleccionados en la captura, se ha creado un fichero backup, que podemos editar, y que contiene el fichero original:


Implementación

Para implementar la clase profilePython he utilizado dos módulos, a saber: os, para la búsqueda de ficheros en el sistema de archivos y time, para crear nombres de ficheros backup únicos a la hora de guardar los datos en el fichero profile.

La clase se compone de 2 atributos ocultos:

__fichero: nombre del fichero que contiene la estructura de fichero profile.
__lineas_fichero: Lista que contiene en cada elemento una línea del fichero. Así cada elemento puede ser una etiqueta de sección o una relación clave = valor.

La clase se compone de los siguientes 3 métodos ocultos ó métodos de soporte (y que son utilizados por los métodos públicos):

def __esvacio(self,cadena): Método que devuelve True si la cadena pasada como parámetro está vacía y False en caso contrario.
def __cargar_fichero(self): Método para cargar la estructura de fichero profile __fichero en el atributo __lineas_fichero.
def __posicionar(self, seccion, clave = None): Método de búsqueda para posicionarse dentro de la estructura guardada en __lineas_fichero.

La clase se compone de los siguientes 3 métodos públicos, anteriormente descritos:

def profile(self, seccion, clave, por_defecto = None): Método para obtener valor de clave en sección.
def set_profile(self, seccion, clave = None, valor = None): Método para crear secciones y claves (y darles valores).
def save_profile(self, backup = True): Método para salvar datos en fichero profile.

Código

El código completo de esta clase se presenta a continuación. Está bastante comentado, por lo que el lector no tendrá, espero, ningún tipo de complejidad en su entendimiento, ya que por otra parte, mi estilo de programación, tampoco lo induce.


# -*- coding: utf8 -*

import os
import time

class profilePython(object):
'''
Clase que implementa el mecanismo de utilización de ficheros
de configuración.
'''
def __init__(self, filename):
'''
Inicialización.
'''
# Nombre del fichero.
self.__fichero = filename
# Líneas del fichero.
self.__lineas_fichero = []
# Carga del fichero.
self.__cargar_fichero()

def __esvacio(self,cadena):
'''
Método que devuelve True si la cadena pasada como parámetro
está vacía y False en caso contrario.
'''
if cadena is None: return True
try:
if len(str(cadena)) == 0: return True
except:
pass
return False

def __cargar_fichero(self):
'''
Carga del fichero en memoria.
'''
try:
# Abrimos el fichero.
fichero = open(os.path.realpath(self.__fichero),'r')
seguir = True
except:
seguir = False
if seguir:
# Volcamos el contenido del fichero a una estructura.
while True:
f = fichero.readline()
if not f: break
if not self.__esvacio(f):
# Quitamos retornos de carro y demás.
f = f.replace("\n","")
f = f.replace("\r","")
f = f.strip()
# Lo añadimos a la lista de líneas.
self.__lineas_fichero.append(f)
# Cerramos el fichero.
fichero.close()

def __posicionar(self, seccion, clave = None):
'''
Método para posicionarse en la sección, clave (opcional).
Devuelve True, posicion de la clave (si encuentra la clave).
Devuelve True, posición de la sección (si encuentra sección y
clave = None).
Devuelve False, -1 (si no encuentra sección).
Devuelve False, posición de la sección (si no encuentra clave).
'''
# Buscamos la sección..
encontrado = False
puntero = 0
for i in self.__lineas_fichero:
puntero += 1
if i.strip() == '['+str(seccion)+']':
encontrado = True
break
if not encontrado: return False, -1
if clave is not None:
# Buscamos la clave.
encontrado = False
for i in range(puntero,len(self.__lineas_fichero)):
# Buscamos el carácter '='.
linea = self.__lineas_fichero[i].split('=')
if len(linea) == 1:
# Nos vamos. La clave no está en esta sección.
break
if len(linea) == 2:
# Miramos si es lo que buscamos.
if str(linea[0]).strip() == str(clave).strip():
# Devolvemos posición.
return True, i
# La clave no existe. Devolvemos posición de la sección.
return False, puntero
else:
# Devolvemos la posición de la sección.
return True, puntero

def profile(self, seccion, clave, por_defecto = None):
'''
Método para obtener valor de clave en sección.
'''
# La sección y clave no pueden estar vacíos.
if self.__esvacio(seccion) or self.__esvacio(clave): return por_defecto
# Buscamos la clave.
seguir, posicion = self.__posicionar(seccion, clave)
if seguir:
# Obtenemos el valor de la clave.
linea = self.__lineas_fichero[posicion].split('=')
try:
return str(linea[1]).strip()
except:
return por_defecto
else: return por_defecto

def set_profile(self, seccion, clave = None, valor = None):
'''
Método para crear secciones y claves (y darles valores).
'''
# Si no hay sección, nos vamos.
if self.__esvacio(seccion): return
# Incluimos una sección.
# Si la sección no existe, se crea.
seguir, posicion = self.__posicionar(seccion)
if not seguir:
self.__lineas_fichero.append('['+str(seccion).strip()+']')
# Asignamos ó incluimos si no existe, el valor
# de una clave a una sección.
if not self.__esvacio(clave):
# Vemos si la (sección,clave) ya existe.
seguir, posicion = self.__posicionar(seccion,clave)
# Formateamos valor.
if valor is None: valor = ''
# Si existe, damos nuevo valor.
if seguir:
self.__lineas_fichero[posicion] = str(clave).strip()+' = '+\
str(valor).strip()
else:
# Si no existe, creamos una nueva clave con su valor.
if posicion != -1:
# Insertamos nueva clave
clave_valor = str(clave).strip()+' = '+str(valor).strip()
self.__lineas_fichero.insert(posicion,clave_valor)

def save_profile(self, backup = True):
'''
Método para salvar datos en fichero profile. Si backup es True
se crea una copia de seguridad del mismo. Devuelve True si se
guardó el fichero y False en caso contrario.
'''
# Formamos etiqueta identificación.
etiqueta = '_backup_'+str(time.time()).strip()
try:
if backup:
# Renombramos fichero.
os.rename(os.path.realpath(self.__fichero),\
self.__fichero+etiqueta)
except: pass
ret = True
try:
# Creamos fichero.
fichero = open(os.path.realpath(self.__fichero),'w')
# Guardamos cada una de las líneas.
for i in self.__lineas_fichero:
linea = str(i) + '\r\n'
fichero.write(linea)
# Cerramos fichero.
fichero.close()
except: ret = False
# Devolvemos estado.
return ret



CONCLUSIONES

En este post hemos visto como implementar la clase profilePython, para el manejo de ficheros de perfil ó de configuración (también conocidos como .INI) de aplicaciones. Este tipo de herramientas son muy frecuentes utilizarlas en el desarrollo de programas, ya que nos da la posibilidad de crear configuraciones personalizadas de las mismas, de una manera fácil y sencilla.

Un cordial saludo.

lunes, 7 de junio de 2010

Calcular tiempos de ejecución, en Python



Hola. Una forma rápida y sencilla de evaluar un algoritmo es calcular el tiempo de ejecución del mismo, para ver si efectivamente es óptimo para nuestros intereses. Este tipo de análisis puede hacerse matemáticamente, ó si disponemos de algún "proceso" que nos indique la rapidez del mismo. No voy a entrar aquí sobre como calcular tiempos de ejecución O, Omega y demás cosas de estas. En este post vamos a ver la forma de calcular el tiempo de ejecución, en segundos, para cualquier algoritmo. Para nuestro ejemplo, mediremos el algoritmo de ordenación QuickSort.

Lo que vamos a crear es, mediante una función decoradora, una función para calcular tiempos de ejecución. Nuestra función de medición es la siguiente:

def cronometro(funcion):
def funcion_a_ejecutar(*argumentos):
# Tiempo de inicio de ejecución.
inicio = time.time()
# Lanzamos función a ejecutar.
ret = funcion(*argumentos)
# Tiempo de fin de ejecución.
fin = time.time()
# Tiempo de ejecución.
tiempo_total = fin - inicio
# Devolvemos el tiempo de ejecución.
return tiempo_total
# Devolvemos la función que se ejecuta.
return funcion_a_ejecutar

Para ver como funciona vamos a crear una lista de números, creados aleatoriamente (para ello vamos a utilizar del módulo random, uniform). El código es el siguiente:

def funcion_generica():
lista_numeros = []
for i in range(1,100000):
numero = int(random.uniform(0,500))
lista_numeros.append(numero)
# Devolvemos lista de números aleatorios.
return lista_numeros

De esta manera lo que vamos a hacer ordenar dicha lista, en varios tipos de clasificación (ascendente y descendente), esto es, vamos a medir el tiempo que tarda el algoritmo QuickSort en realizar dicha tarea. Así la implementación de este algoritmo de clasificación QuickSort es el siguiente:

def quicksort(datos, primero, ultimo, ordenacion = 'asc'):
'''
Método que implementa el algoritmo QuickSort. Utilizado
para clasificación de filas.
'''
i = primero
j = ultimo
posicion_pivote = (primero + ultimo) / 2
pivote = datos[posicion_pivote]
while i <= j:
if ordenacion == 'asc':
while datos[i] < pivote: i+=1
while datos[j] > pivote: j-=1
if ordenacion == 'desc':
while datos[i] > pivote: i+=1
while datos[j] < pivote: j-=1
if i <= j:
aux = datos[i]
datos[i] = datos[j]
datos[j] = aux
i+=1
j-=1
if primero < j:
datos = quicksort(datos, primero, j, ordenacion)
if ultimo > i:
datos = quicksort(datos, i, ultimo, ordenacion)
return datos

Antes de todo hay que importar los módulos necesarios, esto es:

import time
import random

Por último, solo nos falta ejecutar las funciones definidas. Lo primero que hacemos es generar una lista de números aleatorios:

lista_numeros = funcion_generica()

Seguidamente, mediante la función decoradora definida previamente, ordenamos mediante QuickSort, de manera que se mida el tiempo de ejecución.

tiempo1 = cronometro(quicksort)(lista_numeros,0,99998)

Ahora ordenamos con QuickSort, en ordenación descendente, con una lista previamente ordenada ascendente.

tiempo2 = cronometro(quicksort)(lista_numeros,0,99998,'desc')

Y ordenamos con QuickSort, en ordenación ascendente, con una lista previamente ordenada descendente.

tiempo3 = cronometro(quicksort)(lista_numeros,0,99998)

Finalmente vemos el cálculo total de tiempos del algoritmo QuickSort.

print "Cálculo total de tiempos del algoritmo QuickSort"
print "Se ordena una lista de 99999 elementos."
print ""
print "Lista no ordenada: ", tiempo1, " segundos"
print "Lista ordenada ascendente: ", tiempo2, " segundos"
print "Lista ordenada descendente: ", tiempo3, " segundos"

Los resultados que me han dado son los siguientes:


Saludos.