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 <modulo>, pero hay varias maneras de hacerlo:

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:

#!/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.

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.

$ 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:

import sys
sys.path.append('/home/japp/mis_modulos/')

En Windows, funciona de forma idéntica pero usando las rutas de Windows:

sys.path.append('C:\mis_modulos')

Para modificar de forma temporal el PYTHONPATH en Windows haríamos:

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:

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 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 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 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()