Módulos, paquetes y la librería estándar de Python ================================================== Como ya hemos visto, podemos importar nuevos módulos de la librería extándar de Python o de terceros con **import **, pero hay varias maneras de hacerlo: .. code:: python In [10]: import math # importa el módulo math In [11]: import math as M # importa el módulo math llamándolo M In [12]: from math import sin, cos, pi # importa las funciones sin, cos y pi de math In [13]: from math import * # importa todas las funciones de math De manera similar podemos crear un módulo propio que puede usarse como un programa independiente o importase como un módulo y poder reutilizar sus funciones: .. code:: python #!/usr/bin/python3 #-*- coding: utf-8 -*- """Programa de calculo del cubo de un numero""" __author__ = "Jorge" __copyright__ = "Curso de Python" __credits__ = ["Pepe", "José Luis", "Roberto"] __license__ = "GPL" __version__ = "1.0" __email__ = "japp@denebola.org" __status__ = "Development" def cubo(x): """Calcula el cubo de un numero""" y = x**3 return y if __name__ == "__main__": x = int( input("Dame un numero: ") ) y = cubo(x) print("El cubo de %.2f es %.2f" % (x, y)) Bien, ahora podemos usar este programa como un ejecutable como ya hemos hecho o importarlo como un módulo y usar la función cubo(). La primera de comentario multilínea, limitada por comillas triples, se asiga automáticamente a la variable mágica **doc** como la documentación del módulo o programa y el resto de variables especiales como información adicional. Igualmente la primera línea de def() es la documentación de la función. La variable especial **name** es el nombre del módulo cuando se usa como tal, que en este caso vale cubo, pero tiene valor "**main**\ " cuando se ejecuta como un programa. De esta manera distinguimos cuando el código se está ejecutando como un programa o está siendo llamado como un módulo. .. code:: python import cubo In [20]: cubo.__doc__ Out[20]: 'Programa de calculo del cubo de un numero' In [21]: cubo.cubo(3) Out[21]: 27 In [22]: cubo.cubo.__doc__ Out[22]: 'Calcula el cubo de un numero' In [23]: cubo.__version__ Out[23]: '1.0' Para poder importar un módulo nuestro, debe estar en el directorio donde lo estamos llamando, o bien estar en una ruta incluida en el PATH de la librería o bien en la variable PYTHONPATH. .. code-block:: bash $ echo $PYTHONPATH :/home/japp/codigo/lib/:/usr/local/aspylib/:/usr/local/lib/python2.7/dist-packages/ Alternativamente, se puede incluir el **PATH** en el programa ejecutable añadiéndolo a la lista sys.path: .. code:: python import sys sys.path.append('/home/japp/mis_modulos/') En Windows, funciona de forma idéntica pero usando las rutas de Windows: .. code:: python sys.path.append('C:\mis_modulos') Para modificar de forma temporal el ``PYTHONPATH`` en Windows haríamos: .. code:: console C:\>set PATH=C:\Program Files\Python 3.6;%PATH% C:\>set PYTHONPATH=%PYTHONPATH%;C:\mis_modulos C:\>python Si se quiere añadir permanentemente es algo más complicado. Desde el botón de inicio hay que buscar Propiedades del sistema (System properties) -> Advanced system settings y pinchar en el botón de variables de entorno, donde se pueden modificar la variables de entorno del sistema (solo el administrador). Estructura de un paquete de Python ---------------------------------- Los paquetes de python son un espacio de nombres que contiene varios módulos o paquetes, a veces relacionados entre ellos aunque no tiene porqué. Se crean en un directorio que debe incluir obligatoriamente un fichero especial llamado **__init__.py** que es el que indica que se trata de un paquete y luego pueden haber otros módulos e incluso otros paquetes. La siguiente es la estructura típica de un paquete: .. code-block:: bash mi_paquete/ __init__.py modulo1.py modulo2.py utiles/ __init__py utiles1.py config.py El fichero **__init__.py** puede y suele estar vacío, aunque se puede usar para importar modulos comunes entre paquetes. :: import mi_paquete from mi_paquete import utiles1 La librería estándar de Python ------------------------------ La instalación básica de Python viene con una muy completa librería de módulos para todo tipo de tareas, incluyendo acceso a ficheros y directorios, compresión de ficheros, ejecución recurrente (multihilo), email, html, xml, csv y un largo etcétera. Lo más conveniente es consultar la `documentación de la librería estándar `__ para tener una idea de todo lo disponible, pero podemos probar los más importantes. Creación y administración de ficheros ------------------------------------- La forma más directa y práctica de interactuar con el sistema, independientemente de la plataforma, es empleando el módulo ``os``, que básicamente es una interfaz para sistema operativo del ordenador que ejecuta el programa. :: import os os.chdir("/home/japp/Documentos/") os.getcwd() # /home/japp/Documentos/ # Esto no imita a ls, no distingue ficheros y directorios ficheros = os.listdir(".") # hay que poner una ruta for fichero in ficheros: print os.path.isdir(fichero) # .isfile(), islink() Para mayor flexibilidad en la selección de ficheros, por ejemplo usar caracteres comodín, se puede usar el paquete ``glob``: :: from glob import glob ficheros = glob("*.txt") # Son listas también pero con una ruta relativa, así que no funciona igual que listdir ficheros = glob("/home/japp/") # no devuelve nada ficheros = glob("/home/japp/*") # Esto si os.mkdir("registro") # os.makedirs('/home/japp/Documentos/datos/pruebas') # Linux, Mac # os.makedirs('C:\\Mis Documentos\\datos\\pruebas') # Windows os.chmod("registro", 0700) os.rename("registro", "registros") Lectura y escritura de ficheros de texto ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Si queremos leer o escribir ficheros de texto primero hay que abrirlos en el modo adecuado (**r, w, a**) para terner una **instacia del fichero**, que luego se puede leer a ``string`` de varias formas. :: # Leo un fichero CSV con código y nombre de paises fichero = open("paises.csv") contenido = fichero.read() # Lo mete todo en un único string fichero.close() len(contenido) print(contenido[:30]) #'nombre, name, nom, iso2, iso3,' fichero = open("paises.csv") lineas = fichero.readlines() # Lee línea a línea, devuelve una lista fichero.close() len(lineas) 247 De haber querido separar por columnas, pudimos haber usado algo como: :: nombre, name, nom, iso2, iso3, phone_code = lineas.split(";") justo después de ``readlines()``, al hacerlo, ``split()`` devuelve una lista de dos elementos (en este caso) que **desempaquetamos** en las variables que damos a la izquierda. Podemos igualmente escribir un fichero creando un fichero en modo lectura y usar el método ``write(str)`` para guardar una cadena de texto o bien usar ``writelines(lista)`` para guardar el contenido de una lista de strings. ¿Y si el fichero es remoto? hay varias maneras de resolverlo, pero lo más cómodo es con el módulo :mod:`urllib`: :: import urllib.request import csv # Fichero remoto # https://gist.github.com/brenes/1095110 url = "https://gist.githubusercontent.com/brenes/1095110/raw/f8eeb4a7efb257921e6236ef5ce2dbc13c50c059/paises.csv" # Terremotos del día de USGS # url = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv" # Leemos remotamente el fichero csv respuesta = urllib.request.urlopen(url) # Pasamos la instancia a un string contenido = respuesta.read() # o readlines() # Usamos esta vez el módulo csv de Python para interpretar el CSV reader = csv.reader(contenido) Ahora probemos a hacer una selección de los paises que empiezan por "P", pero en su nombre, no en su código :: # Lista de paises que empiezan por P, vacía al principio lineas_P = [] for linea in lineas: codigo, nombre = linea.split(";") if nombre.startswith('P'): lineas_P.append(linea) # Abro el fichero donde voy a guardar f_out = open("paises_P.txt", "w") f_out.writelines(lineas_P) f_out.close() El fichero resultante es un fichero igual que el anterior pero solo con los paises que empiezan con "P", uno por línea, pero es es línea a línea porque el fichero original incluye caracteres de nueva línea. El método ``writelines(lista)`` no escribe a líneas y éstas deben añadirse explícitamente: :: # Lista de numeros enteros, que paso a string y añado nueva línea numeros = [str(n)+"\n" for n in range(100)] f_out = open("numeros.txt", "w") f_out.writelines(numeros) f_out.close() Es posible guardar tambien variable en binario para usarlas después, empleando ``shelve()``: :: import shelve shelf_file = shelve.open('datos') shelf_file['numeros'] = numeros shelf_file.close() # Al cerrar el fichero se guardan los datos, que se pueden recuperar abriendo el fichero. shelf_file = shelve.open('datos') shelf_file['numeros'] El módulo ``os`` tiene otros métodos útiles para interactuar con el sistema y los procresos que ejecuta. :: os.getlogin() #'japp' os.getgroups() #[191, 256, 294, 329, 350, 2000] os.getenv('HOME') os.putenv('HOME', '/scratch/japp') os.uname() # ('Linux', 'vega', '4.1.13-100.fc21.x86_64', '#1 SMP Tue Nov 10 13:13:20 UTC 2015', 'x86_64') Si se desea más información sobre el equipo, se puede emplear el módulo ``platform``, que da información más completa y detallada sobre y ordenador y el SO: :: import platform print('uname:', platform.uname()) print('system :', platform.system()) print('node :', platform.node()) print('release :', platform.release()) print('version :', platform.version()) print('machine :', platform.machine()) print('processor:', platform.processor()) print('distribution:', " ".join(platform.dist()) ) # Linux, mac_ver() para OS X """ uname: ('Linux', 'vega', '4.1.13-100.fc21.x86_64', '#1 SMP Tue Nov 10 13:13:20 UTC 2015', 'x86_64', 'x86_64') system : Linux node : vega release : 4.1.13-100.fc21.x86_64 version : #1 SMP Tue Nov 10 13:13:20 UTC 2015 machine : x86_64 processor: x86_64 distribution: fedora 21 Twenty One """ Si se desea mayor control sobre los ficheros y directorios, el módulo ``shutil`` permite operaciones con ficheros a alto nivel. :: improt shutil shutil.copy('paises.csv', 'paises-copy.csv') # Copia un fichero shutil.copytree("/home/japp/Documentos", "/home/japp/Documentos-copia") # Copia el directorio y su contenido shutil.move('paises-copy.csv', '/home/japp/Documentos/') # Mueve un fichero ¿Cómo borrar ficheros? Existen tres métodos principales: :: os.unlink(path) # Borra el fichero en path os.rmdir(path) # Borra el directorio en path, que debe estar vacío shutil.rmtree(path) # Borra path recursivamente Si queremos borrar con más cuidado podemos usar condicionales: :: for filename in os.listdir("."): if filename.endswith('.csv'): os.unlink(filename) En el ejemplo anterior hemos hecho un listado sencillo del directorio en el que estamos. Para hacer una exploración recursiva de un directorio, distinguiendo en ficheros y directorios, podemos usar ``os.walk()``: :: for directorio, subdirectorios, ficheros in os.walk("/home/japp/Documentos/"): print('El directorio ' + directorio) ``os.walk()`` devuelve una tupla de tres elementos con el nombre del directorio actual, una lista de subdirectorios que contiene y una lista de ficheros que contiene. Con el módulo ``zip`` se pueden leer y escribir ficheros zip: :: fichero_zip = zipfile.ZipFile('datos', 'w') ficheros = ['medidas_PV_He.txt', 'medidas_radio.txt', 'bright_star.tsv'] for fichero in ficheros: newZip.write(fichero, compress_type=zipfile.ZIP_DEFLATED) fichero_zip.close() fichero_zip = zipfile.ZipFile("datos.zip") fichero_zip.namelist() # informacion sobre un fichero en concreto del zip bright_star_info = fichero_zip.getinfo('bright_star.tsv') bright_star_info.file_size # 926482 bright_star_info.compress_size # 248269 # Extraigo el contenido fichero_zip.extract('bright_star.tsv', '/home/japp/Documents/') fichero_zip.extractall() # todos los ficheros fichero_zip.close() Trabajando con fechas y tiempo ------------------------------ La librería estándar de Python incluye varios módulos para tratar y manipular fechas, tiempo e intervalos. Como con otros módulos, una vez importado el módulo se define un objeto específico que permite hacer malabarismos con fechas y tiempo. El módulo principal es ``datetime``, que permite trabajar con fechas y tiempo mientras que el módulo ``time``, ofrece métodos avanzados para tiempo, ignorando la fecha. :: import datetime print("La fecha y hora actuales: " , datetime.datetime.now() # Devuelve un objeto datetime print("Fecha y hora en string con formato: " , datetime.datetime.now().strftime("%Y-%m-%d %H:%M") print("Año actual: ", datetime.date.today().strftime("%Y")) print("Mes del año: ", datetime.date.today().strftime("%B")) print("Semana del año: ", datetime.date.today().strftime("%W")) print("Número de día de la semana: ", datetime.date.today().strftime("%w")) print("Día del año: ", datetime.date.today().strftime("%j")) print("Día del mes: ", datetime.date.today().strftime("%d")) print("Día día de la semana: ", datetime.date.today().strftime("%A")) import time print("Segundos desde inicio de época: %s" %time.time()) # Para una fecha específica fecha = datetime.date(1937, 10, 8) #year, month, day print(fecha.strftime("%A")) # Friday print(fecha.strftime("%b %d %Y %H:%M:%S")) # Oct 08 1937 00:00:00 En el ejemplo anterior usamos el método ``strftime()`` para obtener un string en el formato deseado según `la sintaxis de fechas `__ de Python. De manera similar podemos usar ``strptime()`` para convertir un string de fecha a un objeto ``date`` o ``datetime`` de Python: :: # Fecha en string fecha_str = "2017-05-16 10:30:00" # Formato en el que está la fecha en string fecha_fmt = "%Y-%m-%d %H:%M:%S" # Objeto datetime a partir de la fecha en string fecha = datetime.datetime.strptime(fecha_str, fecha_fmt) print(fecha.strftime("%A %d %B, %Y")) # 'Tuesday 16 May, 2017' # Cambio de idioma import locale idioma = locale.setlocale(locale.LC_TIME, "es_ES") print(fecha.strftime("%A %d %B, %Y")) # martes 16 mayo, 2017 :: # Intervalos de tiempo y operaciones con fechas hoy = datetime.date.today() print('Hoy:', hoy) un_dia = datetime.timedelta(days=1) print('Lapso de un día:', one_day) ayer = hoy - un_dia print('Ayer:', ayer) manhana = hoy + un_dia print('Manhana :', manhana) print('Manhana - ayer:', manhana - ayer) print('Ayer - manhana:', ayer - manhana) ayer > hoy False ayer < hoy True Hay que tener en cuenta que los tiempos se toman de ordenador, incluyendo la zona horaria, por lo que generalmente serán en **hora local**. Si queremos convertir a otra zona horaria, debemos usar el módulo :mod:`pytz`: :: # Hora local canaria actual hora_local = datetime.datetime.now() # datetime.datetime(2017, 5, 12, 10, 30, 0, 379146) # Hora actual en UTC hora_utc = datetime.datetime.utcnow() # datetime.datetime(2017, 5, 12, 9, 30, 0, 226718) from pytz import timezone hora_us_pacific = hora_utc.replace(tzinfo=timezone('US/Pacific')) Finalmente, el módulo :mod:`calendar` ofrece alguna funciones de calendario: :: import calendar cal = calendar.month(2017, 5) print(cal) May 2017 Mo Tu We Th Fr Sa Su 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 29 30 31 print(calendar.TextCalendar(calendar.MONDAY).formatyear(2017, 2, 1, 1, 2)) Llamadas al sistema ------------------- La forma más sencilla de ejecutar comandos sistema, por ejemplo para lanzar programas o ejecutar comandos de la consola es el método ``os.system()`` :: import os os.system('touch /home/japp/Documents') Sin embargo ``system()`` es muy limitado y no permite recoger el resultado la ejecución, de haberla. Mucho más útil y potente es el módulo ``subprocess``: :: import subprocess # Uso básico similar a os.system() subprocess.call(['ls', '-l']) Puesto que los canales de entrada y salida del proceso ``call()`` están ligados a la entrada y salida padre, no puede capturar la salida del comando que ejecuta, como ocurre con ``os.system()``. Si queremos capturar la salida podemos emplear ``check_output()`` y luego procesar el resultado como queramos. :: output = subprocess.check_output(['ps', '-x']) print(output) """ PID TTY STAT TIME COMMAND 3901 ? S 0:00 sshd: invweb@pts/2 3902 pts/2 Ss 0:00 -bash 4248 pts/2 Sl 0:02 gedit cdb_import.py 4527 ? Sl 0:00 /usr/libexec/dconf-service 6134 ? Sl 0:15 /usr/local/apache//bin/httpd -k start 13324 pts/2 Sl+ 0:00 /usr/bin/python /usr/bin/ipython 13613 pts/2 R+ 0:00 ps -x 26515 ? S 0:03 sshd: invweb@pts/0 26516 pts/0 Ss+ 0:00 -bash """ # Separo for filas output_lines = output.split("\n") # Trozo que contiene el comando output_lines[1][27:] # Busco los procesos que usan Python resultados = [] for line in output_lines: if 'python' in line.lower(): resultados.append(line[:5]) # Me quedo con trozo que tiene el PID print(resultados) Usando tuberías directamente podemos usar parámetros para indicar la entrada y salida y capturar errores. Veamos este ejemplo de una función que llama al ``ping`` del sistema: :: def esta_viva(hostname): """ Hace un ping a una maquina para saber si esta conectada """ ping = subprocess.Popen(["ping", "-n", "-c 1", hostname], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, error = ping.communicate() if error == "": print("El ordenador {} está conectado".format(hostname)) return True else: print("El ordenador {} está KO".format(hostname)) return False esta_viva('vega') Conexión remota por FTP y SSH ----------------------------- La librería estándar de Python incluye un módulo ftp con todas las funcionalidades necesarias. Veamos un ejemplo para copiar ficheros locales al FTP público del IAC. :: import ftplib import os from glob import glob # Origen de los datos origin_dir = "/home/japp/Documents/" # directorio destino (en el servidor externo) destination_dir = "in/curso_python" # Lista de los ficheros a copiar, todos los *.py files = glob(origin_dir + "*.py") ftp = ftplib.FTP("ftp.iac.es") login = ftp.login("japp@iac.es", "anonymous") ftp.cwd(destination_dir) os.chdir(origin_dir) for filename in files: infile = open(filename, 'r') ftp.storlines('STOR ' + os.path.basename(filename), infile) infile.close() Hay que fijarse que solo copia ficheros de uno en uno, si se quiere copiar recursivamente hay que implementar una copia recursiva con ``os.walk()`` o similar e ir creado directorios con ``mkdir()``. No hay un módulo específico para ``ssh``, pero se puede usar el del sistema usando el módulo ``pexpect``, que permite manejar envío de información entre un servidor y un cliente, en ambas direcciones. :: import pexpect import time host = "vega" password = "secreto" ssh_newkey = 'Are you sure you want to continue connecting' ssh = pexpect.spawn("ssh {}".format(host)) i = ssh.expect([ssh_newkey, 'password:', host, pexpect.EOF], timeout=10) if i == 0: ssh.sendline('yes') # Se dan una lista de posibles salidas del comando: nueva key, # la contraseña o el prompt del sistema si no pide contraseña i = ssh.expect([ssh_newkey, 'password: $', pexpect.EOF]) ssh.sendline(password) ssh.expect(pexpect.EOF) elif i == 1: ssh.sendline(password) ssh.expect(pexpect.EOF, timeout=10) elif i == 2: pass # Extraemos el resultado del comando p = pexpect.spawn("df -h") print(p.read()) ssh.close()