martes, 16 de octubre de 2018

Pathlib: rutas orientadas a objetos



El módulo pathlib disponible desde Python 3.4 consta de clases que permiten representar rutas de archivos y directorios de un sistema de ficheros con la semántica adecuada para distintos sistemas operativos.

Las clases cuentan con métodos que facilitan una amplia variedad de operaciones con directorios, archivos y enlaces simbólicos; que antes eran posibles, la mayoría, recurriendo a funciones y clases de varios módulos (como os, glob, shutil y fileinput). Lo que se ha pretendido con pathlib es concentrar en un módulo las operaciones más comunes teniendo en cuenta la experiencia de los desarrolladores.

Dentro de las operaciones contempladas las hay para explorar el sistema de ficheros, comprobar la existencia de rutas; recorrer los archivos y subdirectorios de un directorio filtrando los objetos mediante el uso de patrones; obtener información de los objetos (tamaño, permisos, propietarios, etc.); crear, renombrar y borrar archivos y enlaces simbólicos; crear y borrar directorios; obtener rutas del sistema en varios formatos (Posix y URI), principalmente.


Rutas puras y concretas


Las clases de pathlib se dividen en dos grupos en función a los tipos de rutas orientadas a objetos que manejan: puras y concretas. La diferencia entre ambos tipos de clases está en que las clases para rutas puras proporcionan métodos que en realidad no acceden al sistema de ficheros y las de rutas concretas, sí.

Las rutas son inmutables y por ser únicas en cada sistema de archivos se les pueden aplicar funciones hash. Las rutas de un mismo sistema se pueden comparar y ordenar de acuerdo a la semántica empleada en cada sistema.

Cualquier ruta se representa como una cadena adaptada a las rutas propias de cada sistema de archivos y, lógicamente, se podrán utilizar con cualquier función que opere con rutas en este mismo formato.

Hay tres formas de instanciar objetos de rutas puras:

  • class pathlib.PurePath(*pathsegments): clase genérica que crea una instancia de ruta en función del sistema operativo: puede ser PurePosixPath o un PureWindowsPath.
  • class pathlib.PurePosixPath(*pathsegments): subclase que crea una instancia de ruta para sistemas de ficheros no Windows.
  • class pathlib.PureWindowsPath(*pathsegments): subclase que crea una instancia de ruta para sistemas de ficheros Windows.

A continuación, se declaran varias rutas puras y se muestran varios ejemplos que ilustran el uso de sus métodos:

import os
from pathlib import PurePath, PurePosixPath, PureWindowsPath

# Declarar rutas puras

ruta1 = PurePath('home/usuario')
ruta2 = PurePath('c:\\windows')
ruta3 = PurePosixPath('home/Usuario')
ruta4 = PureWindowsPath('C:\\WINDOWS')

# Imprimir las rutas

print(ruta1)  # home/usuario
print(ruta2)  # c:\windows
print(ruta3)  # home/Usuario
print(ruta4)  # C:\WINDOWS

# Las rutas son cadenas de un tipo especial

print(type(ruta1))  # 
print(type(ruta2))  # 
print(type(ruta3))  # 
print(type(ruta4))  # 

# Las rutas se pueden comparar ...

print(ruta1 == ruta3)  # False
print(ruta2 == ruta4)  # True (en Windows)
print(ruta2 in [ruta4])  # True (en Windows)
print(PureWindowsPath('c:\\windows') > ruta4)  # False
if os.name != 'posix':
    print(ruta2 > ruta4)  # False (en Win); Error (en No Win)

# Una ruta se representa como una cadena adaptada a
# las rutas propias de cada sistema de archivos

ruta5 = PurePath('/var/www/index.html')
ruta6 = PureWindowsPath('c:/Program Files')

print(ruta5)  # /var/www/index.html
print(ruta6)  # c:\Program Files

# Obtener por separado en una tupla todas las partes de una ruta

print(ruta5.parts)  # ('/', 'var', 'www', 'index.html')
print(ruta6.parts)  # ('c:\\', 'Program Files')

# Obtener la unidad (si existe) de una ruta

print(ruta5.drive)  # ''
print(ruta6.drive)  # 'c:'

# Obtener la raíz de una ruta. Válido para rutas UNC

print(ruta5.root)  # '/'
print(ruta6.root)  # '\'

# Obtener la raíz y la ruta ¡de una vez!

print(ruta5.anchor)  # '/'
print(ruta6.anchor)  # 'c:\'

# Obtener los ancestros de una ruta

print(ruta5.parents)  # 
print(len(ruta5.parents))  # Número de ancestros: 3
print(ruta5.parents[0])  # /var/www
print(ruta5.parents[1])  # /var
print(ruta5.parents[2])  # /

# Obtener el ancestro ineidato o padre de una ruta

print(ruta5.parent)  # /var/www
print(ruta6.parent)  # c:\

# Obtener nombre y extensión/es de archivos

print(ruta5.name)  # index.html
print(ruta5.suffix)  # .html
print(ruta5.suffixes)  # ['.html']
print(ruta5.stem)   # index

# Obtener rutas en formato POSIX y URI

print(ruta6.as_posix())  # c:/Program Files
print(ruta6.as_uri())  # file:///c:/Program%20Files

# Obtener si una ruta es absoluta: si tiene raíz o unidad

print(ruta5.is_absolute())  # True
print(ruta6.is_absolute())  # True

# Combinar rutas

print(ruta6.joinpath('Lupa'))  # c:\Program Files\Lupa
print(ruta6)  # c:\Program Files

# Comprobar si un patrón existe en una ruta

print(ruta5.match('*.html'))  # True
print(ruta5.match('var/*.html'))  # False

# Obtener la ruta relativa (si es posible)

print(ruta5.relative_to('/var'))  # www/index.html
print(ruta5.relative_to('/var/www'))  # index.html

# A partir de una ruta obtener otra ruta cambiando
# nombre completo de archivo o extensión (si es posible)

print(ruta5.with_name('imagen.jpg'))  # /var/www/imagen.jpg
print(ruta5.with_suffix('.htm'))  # /var/www/index.htm

Por otra parte, los objetos de ruta concreta pertenecen a subclases de las clases de rutas puras. Estas subclases además de ofrecer las operaciones proporcionadas por las clases de las que descienden cuentan con métodos para instancias de rutas que hacen llamadas al sistema. Hay tres formas de instanciar estos objetos:

  • class pathlib.Path(*pathsegments): es una subclase de la clase PurePath que crea una instancia de ruta concreta en función del sistema operativo: puede ser PosixPath o WindowsPath.
  • class pathlib.PosixPath(*pathsegments): es una subclase de la clase PurePosixPath que crea una instacia de ruta concreta para sistemas de ficheros no Windows.
  • class pathlib.WindowsPath(*pathsegments): es una subclase de la clase PureWindowsPath que crea una instancia de ruta concreta para sistemas de ficheros Windows.

A continuación se declaran varias rutas concretas y se ofrecen varios ejemplos que muestran el uso de sus métodos:

from pathlib import Path, PosixPath, WindowsPath

# Declarar rutas concretas

ruta1 = Path('home/usuario')
ruta2 = Path('c:\\windows')
ruta3 = PosixPath('home/Usuario')
ruta4 = WindowsPath('C:\\WINDOWS')  # Error en Linux

# Imprimir las rutas

print(ruta1)  # home/usuario
print(ruta2)  # c:\windows
print(ruta3)  # home/Usuario
print(ruta4)  # C:\WINDOWS

# Las rutas son cadenas de un tipo especial ...

print(type(ruta1))  # 
print(type(ruta2))  # 
print(type(ruta3))  # 
print(type(ruta4))  # 

# Obtener ruta del directorio actual de trabajo

print(Path.cwd())  # /home/usuario/Directorio

# Obtener la ruta del directorio de usuario actual

print(Path.home())  # /home/usuario

# Obtener información del directio o del archivo

# Obtener tamaño en bytes

print(Path('/etc/os-release').stat().st_size)  # 386

# Obtener tiempo de modificación (expresado en segundos)

print(Path('/etc/os-release').stat().st_mtime)  # 1534722298.0

# Para enlaces simbólicos: lstat()

# Cambiar los atributos y/o permisos del directorio o del archivo

Path('/home/usu1/imagen1.png').chmod(0o666)

# Para enlaces simbólicos: lchmod()

# Comprobar si existe el archivo o directorio

print(Path('/etc/os-release').exists())  # True

# Expandir la ruta añadiendo la ruta 'home' del usuario actual

print(Path('~/bashrc').expanduser())  # /home/usuario/bashrc

# Obtener de la ruta una lista con los archivos del patrón

archivos = Path('/home/usu1/Descargas').glob('*.pdf')
for archivo in archivos:
    print(archivo)

# Otros patrones:
# '*/*.pdf' Obtener del directorio con 1 nivel de profundidad
# '**/*.pdf' Obtener del directorio actual y sus subdirectorios
# El patrón '**/*.pdf' es equivalente a rglob('*.pdf')

# Obtener nombre del grupo propietario

print(Path('/etc/os-release').group())  # root

# Conocer si una ruta es un directorio o un enlace simbólico
# que apunta a un directorio existente

print(Path('/etc/os-release').is_dir())  # False

# Conocer si una ruta es un fichero o un enlace simbólico
# que apunta a un fichero existente

print(Path('/etc/os-release').is_file())  # True

# Conocer si una ruta es un enlace simbólico

print(Path('/etc/resolv.conf').is_symlink())  # True

# Conocer si la ruta es un punto de montaje.
# No implementado para windows. Nuevo en Python 3.7

print(Path('/media/usuario').is_mount())  # True

# Obtener iterador con objetos contenidos en un directorio

for elemento in Path('/home/usu1/Descargas').iterdir():
    print(elemento)  # Lista ficheros y directorios

# Crear un directorio (si no existe)

Path('/home/u1/bd').mkdir(mode=0o777, parents=False, exist_ok=True)

# Si parents=True creará los directorios padres que falten 
# en el camino
# Si exist_ok=True y el directorio a crear existe se 
# omitirá mensaje de error

# Abrir un archivo para leer y/o escribir

with Path('/etc/os-release').open(mode='r') as archivo:
    for linea in archivo:
        print(linea.strip())

# Leer y escribir de/en un archivo de texto

archivo = Path('secreto.txt')
archivo.write_text('Este archivo guarda un gran secreto')
print(archivo.read_text())

# Leer y escribir de/en un archivo en formato binario

archivo = Path('secreto.dat')
archivo.write_bytes(b'Este archivo guarda un gran secreto')
print(archivo.read_bytes())

# Conocer el propietario de un archivo

print(Path('/etc/resolv.conf').owner())  # root

# Renombrar un archivo o un directorio

Path('/home/usu1/imagen1.png').rename('/home/usu1/imagen2.png')

# En Unix si se trata de un archivo existeste será reemplazado.

# Renombrar un archivo o un directorio.

Path('/home/usu1/imagen2.png').replace('/home/usu1/imagen1.png')

# Si existe archivo o directorio será reemplezado

# Convertir una ruta en absoluta

print(Path('/etc/resolv.conf').resolve())  
# /run/resolvconf/resolv.conf

print(Path('..').resolve())  # /home/usuario/Local

# Borrar un directorio vacío

dir1 = Path('/home/usu1/temp-1')
if dir1.exists():
    dir1.rmdir()

# Conocer si hay coincidencia en dos rutas

documentos = '/home/usu1/.bashrc'
print(Path('/home/usu1/.bashrc').samefile(documentos))  # True

# Crear un enlace simbólico

Path('MiEnlace').symlink_to('imagen.png')

# Borrar un archivo o un enlace simbólico

Path('imagen.png').unlink()




Ir al índice del tutorial de Python

sábado, 6 de octubre de 2018

El depurador PuDB




PuDB es un depurador ligero de código para Python con una interfaz de texto que proporciona todo lo necesario para probar programas y encontrar los errores de lógica que puedan producirse mientras se ejecutan.

Durante la ejecución del código se aplica color a su sintaxis para facilitar su lectura y se muestra información en tiempo real de las variables (y de otros objetos), de la pila de ejecución y de los puntos de parada que se establezcan para el análisis. Todo ello, distribuido en áreas separadas que favorecen su seguimiento. También, existe un área a la que es posible acceder en cualquier momento para ejecutar comandos Python o acceder a uno de los Shell disponibles (ipython, ptpython, bpython, etc).

La navegación entre las distintas áreas y el modo de interactuar con el depurador se hace mediante la pulsación de teclas lográndose que la tarea de depuración sea fácil y rápida.


Instalación de PuDB


Instalación con pip:

$ pip3 install pudb


Uso básico de PuDB


Para explicar el funcionamiento del depurador utilizaremos el código del siguiente ejemplo que incluye una función funcion1, a todas luces innecesaria, y varias variables que modifican su valor a medida que se ejecutan dos bucles while. Se trata de ejecutar el programa y hacer un seguimiento de sus distintos objetos para localizar un error y subsanarlo. El programa que cuenta con dos contadores (var1 y var2) debe presentar en pantalla sus valores mostrando como var1 se va incrementando de 1 en 1 (desde 1 a 10) cada vez que var2 completa un ciclo que va desde 10 a 1 (decreciendo de 1 en 1). También hay una variable de tipo lista lista1 a la que se va añadiendo el valor que va tomando var1 en cada ciclo, que se muestra al finalizar.

bucles.py:

def funcion1(par1):
    return par1 + 1
    
var1 = 0
lista1 = []

while var1 < 10:
    var2 = 10    
    var1 = funcion1(var1)
    lista1.append(var1)    
    while var2 > 0:
        print(var1, var2)
        var2=-1

print(lista1)

Si ejecutamos el código con:

$ python3 bucles.py

Comprobaremos que la salida que se obtiene es errónea, que no se corresponde con lo esperado. En ella var1 se incrementa de 1 en 1 pero var2 se mantiene siempre con el valor 10. Según lo comentado con anterioridad antes de que var1 cambie al siguiente valor, var2 tiene que ir desde 10 a 1, decreciendo de 1 en 1.

Salida errónea:

1 10
2 10
3 10
4 10
5 10
6 10
7 10
8 10
9 10
10 10
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Para comenzar la depuración del programa insertar al principio del código la siguiente línea:

from pudb import set_trace

Después, iniciar el depurador con:

$ pudb3 prueba.py

La primera vez que se inicia el depurador se muestra la ventana de bienvenida con información general de la versión. Para avanzar presionar [return].




Después hay que configurar las preferencias del depurador en la ventana Edit Preferences: básicamente, entre sus opciones hay que seleccionar si se van a mostrar o no los números de líneas junto al código, si habrá que confirmar o no la salida del depurador, el Shell Python que se utilizará de forma predeterminada, el tema o combinación de colores de la interfaz, el orden de la pila de ejecución y el modo de mostrar las variables.

Para desplazarse entre las opciones utilizar las fechas de desplazamiento del cursor [arriba] y [abajo] y para seleccionar o deseleccionar una opción la [barra espaciadora]. Cuando se hayan establecido todas las opciones, para aceptarlas, presionar la flecha [derecha] para situar el cursor en la ventana de las opciones OK y Cancel. Seleccionar OK y confirmar con [return].

Más adelante, cuando se desee acceder a la ventana Edit Preferences para cambiar alguna opción, presionar [Control+p]. Toda la información de la configuración se guarda en el archivo ~/.config/pudb/pudb.cfg.

A continuación, se mostrará en pantalla la interfaz de PuDB con el código del programa a depurar en el área mayor; a su derecha se muestran las áreas para seguimiento de Variables, Stack y Breakpoints; y en la parte inferior el área Command line para ejecutar comandos Python.

Antes de iniciar la depuración podemos insertar uno o más puntos de parada en las líneas de código que consideremos más adecuadas para la exploración. Para ello, utilizar las teclas de flecha [arriba] y [abajo] para desplazar el selector de línea y presionar [b] para insertar o eliminar un punto de parada.




En este caso desplazar el selector de línea a la línea 14 ("print(var1, var2)") utilizando la tecla de flecha [abajo] y presionar la tecla [b] para insertar un punto de parada (la línea aparecerá con el color rojo de fondo). Después, para iniciar la ejecución desde la primera línea hasta alcanzar dicho punto presionar [c].

En este momento el programa se encuentra detenido en la línea 14. En la ventana Variables aparecen ya los valores actuales de las variables e información de otros objetos: var1 = 1, var2 = 10, etc. A partir de ahora podemos avanzar línea a línea presionando la tecla [n]. A medida que pulsamos y completamos ciclos observamos en el área Variables que el valor de var2 cambia su valor de 10 a -1, alternando estos valores en vez de ir decreciendo de 1 en 1 como estaba previsto. Concretamente, al pasar por la línea 15 se asigna erróneamente el valor -1, cuando en realidad habría que restar 1 a var2. Para subsanarlo salir del depurador presionando la tecla [q] y cambiar el código de la línea 15 que es 'var2=-1' por 'var2-=1'. Después, probar la ejecución y el resultado debe ser correcto. Si suprimimos el punto de parada actual presionando la tecla [b] en la línea 14 y después presionamos la tecla [c] el programa se ejecutará hasta el final.

Una ventaja de PuDB es que "recuerda" los puntos de parada para futuras sesiones porque se guardan en el archivo ~/.config/pudb/saved-breakpoints. Estos puntos de parada también pueden establecerse insertando en el código la función set_trace().

Durante la depuración para cambiar al área Variables presionar [V]. En este área se puede modificar el modo de visualizar un objeto. Por ejemplo, en vez de visualizar el número de elementos que contiene una lista se pueden ver los propios elementos.

Para cambiar entre las áreas se pueden usar las teclas de desplazamiento del cursor en cualquier dirección. Por ejemplo, al presionar la tecla de flecha [abajo] cada vez que se alcance el límite inferior de un área se pasará al área inmediata de abajo. Esto funciona en cualquier sentido según se presione flecha [arriba], [abajo], [izquierda] o [derecha]. Otra alternativa para cambiar de área directamente es mediante las teclas siguientes:

  • [C] - Code 
  • [V] - Variables 
  • [S] - Stack 
  • [B] - Breakpoints

También, desde cualquier área se puede pasar al área Command Line para ejecutar comandos Python presionando [Control-x] o pasar de ésta al área del código con la misma combinación de teclas.

Por último, si se desea iniciar desde PuDB el Shell configurado en las opciones de preferencia presionar [!]. Y para ver información de ayuda de otros usos del teclado pulsar [?].

Hay distintas alternativas para depurar programas Python pero para los amantes del Terminal y, en particular, de las interfaces de texto en PuDB encontrarán un buen aliado.

Relacionado:


Ir al índice del tutorial de Python