Mostrando entradas con la etiqueta Python3. Mostrar todas las entradas
Mostrando entradas con la etiqueta Python3. Mostrar todas las entradas

martes, 12 de mayo de 2020

Data Classes: Clases de datos

Data Classes


Una de las características más interesantes de Python 3.7 es el soporte que proporciona el módulo dataclasses con el decorador dataclass para escribir clases de datos.

En una clase de datos se generan automáticamente algunos métodos especiales para clases simples. Los nombres de estos métodos, también llamados métodos mágicos, comienzan y finalizan con un doble subrayado como __init__(), __repr__(), __eq__(), entre otros.

Como es sabido el método __init__() se utiliza en una clase para inicializar un objeto y se invoca sin hacer una llamada específica, simplemente, cuando se instancia una clase. De ahí, que se le conozca como método constructor.

De modo que escribir una clase como la del siguiente ejemplo era lo normal hasta hace muy poco. En este caso la acción de instanciar la clase para crear un objeto lleva implícita la llamada al método __init__() que efectúa las asignaciones de nombre, altura y peso. Por ello, cuando se imprime la altura se obtiene el valor asignado sin que sea necesario hacer nada más:

class Deportista:
    def __init__(self, nombre, altura, peso):
        self.nombre = nombre
        self.altura = altura
        self.peso = peso

deportista1 = Deportista('Elena', 1.81, 64)
print(deportista1.altura)  # 1.81

Bien, la nueva característica que comentamos permite ahora escribir la clase anterior de forma más simplificada y clara:

from dataclasses import dataclass

@dataclass
class Deportista:
    nombre: str
    altura: float
    peso: float

deportista1 = Deportista('Elena', 1.81, 64)
print(deportista1.altura)  # 1.81

Como puede observarse a la clase Deportista le precede el decorador dataclass y no tiene definido el método __init__().

Una de las funciones del decorador es localizar las variables de clase que llevan anotaciones de tipos para conocer los campos que tiene la clase de datos. Después, con respecto al modo de instanciar la clase no se advierte ningún cambio con respecto al uso habitual.

Los métodos de dataclass


La magia obviamente está en el decorador de clase que ayuda a reducir el código porque no solo genera el método __init__(), también hace lo propio con los métodos __str__(), __repr__() y, opcionalmente, con algunos métodos más.

Y sabemos que el decorador genera el método __str__() (que devuelve una cadena con una representación legible de los datos) porque es llamado cuando se imprime el objeto o cuando se hace uso de la función str():

print(deportita1)  # Deportista(nombre='Elena', altura=1.81, peso=64)

atleta = str(deportista1)  
print(atleta)  # Deportista(nombre='Elena', altura=1.81, peso=64)

Algunos de estos métodos también pueden reescribirse dentro de la clase para modificar su comportamiento predeterminado. En el ejemplo siguiente el método __str__() se ha reescrito y devuelve una cadena con el siguiente formato: 'nombre: altura, peso'

@dataclass
class Deportista:
    nombre: str
    altura: float
    peso: float
    
    def __str__(self) -> str:
        return f'{self.nombre}: {self.altura}, {self.peso}'

deportista1 = Deportista('Elena', 1.81, 64)
print(str(deportista1))  # Elena: 1.81, 64

Los parámetros de dataclass


El decorador dataclass cuenta también con varios parámetros para ajustar su funcionamiento:

@dataclass(init=True, repr=True, eq=True, order=False,
           unsafe_hash=False, frozen=False)
  • init, repr y eq: Por defecto estos parámetros tienen el valor True para que el decorador genere los métodos __init__(), __repr__() y __eq__(), respectivamente, aunque si la clase los redefine serán ignorados.
  • order: Por defecto tiene el valor False pero si se establece a True el decorador generará los métodos especiales __gt__(), __ge__(), __lt__() y __le__(). En este caso no se permite la reescritura, por lo que si la clase redefine alguno de ellos se producirá una excepción.
  • unsafe_hash: Por defecto tiene el valor False y en este caso el decorador generará el método __hash__() de acuerdo a la configuración que tengan los parámetros eq y frozen.
  • frozen: Por defecto tiene el valor False pero si se establece a True cualquier intento de asignación a los campos producirá una excepción.

En el siguiente ejemplo se establece el parámetro order con el valor True para que el decorador dataclass genere los métodos __gt__(), __ge__(), __lt__() y __le__() que se corresponden con las comparaciones "mayor que", "mayor o igual que", "menor que" y "menor o igual que", respectivamente.

Las variables de clase son inicializadas cuando los objetos se crean omitiendo dichos valores. En este ejemplo se crean tres objetos asignando un valor al campo peso para realizar comparaciones y conocer si el valor del campo en un objeto es "mayor que" en otro. Y sabemos que el método __gt__() se ha generado porque es llamado cuando se comparan los objetos con el operador ">":

@dataclass(order=True)
class Deportista:
    nombre: str = 'Desconocido'
    altura: float = 0
    peso: float = 0

deportista1 = Deportista(peso=64)
deportista2 = Deportista(peso=62)
deportista3 = Deportista(peso=67)

print(deportista1 > deportista2)  # True
print(deportista1 > deportista3)  # False

Ahora es suficiente con cambiar el valor de order a False para verificar que en ese caso los métodos no están disponibles y que se produce una excepción porque la comparación "mayor que" no estaría soportada por la clase.

En el ejemplo siguiente se establece el parámetro frozen a True con lo cual es posible instanciar la clase para crear objetos pero no es posible asignar valores porque el objeto ha sido "congelado". El intento de asignación produce una excepción de tipo dataclasses.FrozenInstanceError:

@dataclass(frozen=True)
class Deportista:
    nombre: str = 'Desconocido'
    altura: float = 0
    peso: float = 0

deportista1 = Deportista(peso=64)
deportista1.peso = 63  # dataclasses.FrozenInstanceError

La función asdict()


La función asdict() se utiliza para convertir una instancia de clase de datos en un diccionario Python.

En el ejemplo siguiente se importa la función asdict que se emplea para convertir el objeto deportista1 en un diccionario usando los campos de la clase de datos para definir sus claves y sus valores:

from dataclasses import dataclass, asdict

@dataclass
class Deportista:
    nombre: str
    altura: float
    peso: float

deportista1 = Deportista('Elena', 1.81, 64)
dicc1 = asdict(deportista1)

if dicc1['altura'] > 1.75:
   print(dicc1['nombre'], 'supera la altura')

La función field()


La función field() permite facilitar información adicional al decorador relativa a cada campo que la utilizará en la generación de los métodos.

En el ejemplo que sigue para el atributo peso se establecen los parámetros init y repr a False. Esto indica al decorador que el objeto podrá crearse sin el atributo peso y que cuando se imprima su representación será omitida esta información. No obstante, como el atributo peso existe se le podrá asignar un valor en cualquier momento y acceder al mismo después de la asignación.

from dataclasses import dataclass, field

@dataclass
class Deportista:
    nombre: str
    altura: float
    peso: float = field(init=False, repr=False)

deportista1 = Deportista('Elena', 1.81)
deportista1.peso = 64
print(deportista1)  # Deportista(nombre='Elena', altura=1.81)
print(deportista1.peso)  # 64

Herencia


Las clases de datos también pueden heredar atributos y métodos de otras clases de datos.

En el siguiente ejemplo la clase de datos Equipo hereda de la clase Deportista sus variables y métodos aunque en esta ocasión ambas clases redefinen el método __str__() para que al ser llamado muestre información diferente en cada ámbito.

En la clase que hereda, Equipo, la variable equipo debe tener un valor por defecto para que cuando se instancie la clase Deportista no se produzca una excepción de tipo TypeError. Esto es así, aún cuando el atributo equipo queda fuera del alcance de la clase Deportista.

from dataclasses import dataclass

@dataclass
class Deportista:
    nombre: str
    altura: float = 0
    peso: float = 0

    def __str__(self) -> str:
        return f'{self.nombre}: {self.altura}, {self.peso}'

@dataclass
class Equipo(Deportista):
    equipo: str = 'desconocido'

    def __str__(self) -> str:
        return f'{self.nombre}: {self.equipo}'

# Instancia la clase Deportista para crear objeto:
deportista1 = Deportista('Elena', 1.81, 64)

# Imprime llamando al método __str__() de
# la clase Deportista:
print(deportista1)  # Elena: 1.81, 64

# Instancia la clase Equipo para crear objeto:
deportista2 = Equipo('Marta', equipo='Sevilla')

# Imprime llamando al método __str__() de
# la clase Equipo:
print(deportista2)  # Marta: Sevilla

# Asigna valores a atributos de objeto de la clase Equipo:
deportista2.altura = 1.76
deportista2.peso = 68

# Imprime representación formal de objeto de la clase Equipo:
print(repr(deportista2))

# Equipo(nombre='Marta', altura=1.76, peso=68, equipo='Sevilla')


Relacionado:

martes, 25 de febrero de 2020

Expresiones de asignación (:=)



Las expresiones de asignación es una novedad que incorpora la sintaxis de Python 3.8 para asignar valores a variables que son parte de una expresión, evitándose con ello el tener que inicializarlas con antelación.

Para este tipo de asignación se utiliza dentro de la expresión el operador morsa := (walrus en inglés) que añade claridad y simplicidad al código.

A continuación, varios casos de uso:

# Inicializar una variable antes de utilizarla
# es lo que siempre hemos hecho:

edad = 18
print(edad)  # 18

# Ahora se puede inicializar en una expresión
# que además es evaluada inmediatamente:

print(edad:=18)  # 18


# Antes, para guardar el valor de una
# función se asignaba previamente a una
# variable y después esta se podía utilizar: 

def media(nota1, nota2):
    return (nota1 + nota2) / 2

   
notafinal = media(6, 8)
if notafinal >= 5:
    print('Aprobado con:', notafinal)

# Ahora las dos acciones se pueden
# hacer en una misma línea:

if (notafinal:=media(6, 8)) >= 5:
    print('Aprobado con', notafinal)
    

# Antes para hacer una asignación en
# un bucle se hacía dentro del mismo:

lista_compra = list()
while True:
    articulo = input('¿Qué necesitas comprar?: ')
    if articulo == '':
        print(lista_compra)
        break
    
    lista_compra.append(articulo)

# Ahora la misma asignación se puede hacer
# en la misma línea de while:

lista_compra = list()
while (articulo := input('Qué necesitas comprar?: ')) != '':
    lista_compra.append(articulo)

print(lista_compra)

Las expresiones de asignación también se pueden utilizar en listas de comprensión:

parcelas_m2 = [220, 320, 180, 430]
precios = [(precio_m2:=100) * sup for sup in parcelas_m2] 
print(f'{precios} al precio de {precio_m2} € el m2')

# [22000, 32000, 18000, 43000] al precio de 100 € el m2

El uso de paréntesis en las expresiones de asignación es fundamental para delimitar exactamente el valor que se asigna a una variable:

# En el ejemplo que sigue a la variable no se
# asigna el valor 10, se asigna el resultado
# de comparar 10 y 5, es decir, True

if precio:=10 > 5:
    print(precio)  # True

# En este caso queda más claro que es 10 el
# valor asignado a la variable precio:

if (precio:= 10) > 5:
    print(precio)  # 10


# Los paréntesis también permiten anidar expresiones
# para una asignación múltiple:

(total:= (precio:=10) * 5)
print(precio, total)  # 10


# Pero atención, recuerda que existe una diferencia
# básica entre el uso de = y := en una asignación:

# En el siguiente ejemplo se asigna una tupla
# de 3 valores a la variable:

var1 = 0, 1, 2
print(var1)  # (0, 1, 2)

# Y en la siguiente expresión se asigna solo el
# primero de los valores: 0

(var1 := 0, 1, 2)
print(var1)  # 0

# Lo recomendable en estos casos es delimitar
# también por paréntesis los valores de la tupla:

(var1 := (0, 1, 2))
print(var1)

# Con delimitar solo los valores de la tupla no
# es suficiente. Hacerlo genera un error de
# sintaxis:

var1 := (0, 1, 2)  # SyntaxError



domingo, 23 de febrero de 2020

Funciones que imponen nombrar o no parámetros




Cuando se llama a una función Python con parámetros se pueden pasar los valores atendiendo a la posición de los parámetros o referenciando el nombre de los mismos.

A partir de Python 3.8 al declarar una función además puede establecerse qué parámetros, cuando se llame a la función, deben indicarse con su nombre y cuáles no.

Una opción consiste en introducir una barra inclinada hacia la derecha '/' en la definición de la función, entre los parámetros, para obligar a que todos aquellos que se encuentren a su izquierda no puedan referenciar su nombre cuando la función es llamada:

# En este primer ejemplo se muestran los modos habituales
# de llamar a una función (por referencia posicional o por
# nombre): 

def funcion1(x, y, z):
    return x + y + z

print(funcion1(1, 2, 3))  # 6
print(funcion1(x=1, y=2, z=3))  # 6
print(funcion1(z=3, x=1, y=2))  # 6


# En los siguientes casos todos los parámetros a la 
# izquierda de la barra '/' deben indicarse sin nombrar. 
# Si no es así se producirá una excepción de 
# tipo TypeError:

def funcion2(x, y, z, /):
    return x + y + z

print(funcion2(1, 2, 3))  # 6 (es correcta la llamada)
print(funcion2(x=1, y=2, z=3))  # Genera el error siguiente:
# TypeError: funcion2() got some positional-only arguments 
# passed as keyword arguments: 'x, y, z'


def funcion3(x, /, y, z):
    return x + y + z

print(funcion3(x=1, y=2, z=3))  # Genera el error siguiente:
# TypeError: funcion3() got some positional-only arguments 
# passed as keyword arguments: 'x'
# El parámetro 'x' por estar a la izquierda de la barra '/' 
# necesariamente debe indicarse sin nombrar:
print(funcion3(1, z=2, y=3))  # 6

Otra posibilidad es incluir un asterisco '*' entre los parámetros para imponer que todos los que se encuentren a su derecha tengan que referenciar su nombre:

def funcion4(x, y, *, z):
    return x + y + z

print(funcion4(x=1, y=2, z=3))  # 6 (es correcta esta llamada)
print(funcion4(1, 2, 3))  # Genera la siguiente excepción:
# TypeError: funcion4() takes 2 positional arguments 
# but 3 were given
# El parámetro 'z' por estar a la derecha del '*' debe
# ser referenciado con su nombre:
print(funcion4(1, 2, z=3))  # 6

Esta nueva funcionalidad puede resultar de utilidad cuando entre distintas versiones de una API existan funciones que han variado el número de sus parámetros y, por razones de compatibilidad, se prefiera imponer a los desarrolladores el nombrar o no a conveniencia determinados parámetros.



Relacionado:

domingo, 8 de septiembre de 2019

Programas con estilo en Python




Dentro de los documentos que recogen las propuestas de mejora del lenguaje Python (PEP) se encuentra la Guía de estilo para código Python (PEP 8) que contiene un conjunto de convenciones y pautas dirigidas a los programadores para mejorar la legibilidad de los programas y dotar los proyectos de cierta consistencia y coherencia.

Seguir las recomendaciones de PEP 8 además de garantizar código limpio y ordenado, beneficia a todas las personas que trabajan en un mismo proyecto facilitando, en especial, la labor de los programadores cuando tienen que leer y comprender el código escrito por otras personas; incrementando el rendimiento y la imagen de profesionalidad de los equipos.

Sin más preámbulo pasamos a ver las recomendaciones más importantes de PEP 8 y algunas herramientas útiles para revisar y corregir el estilo de nuestros programas.


Recomendaciones de PEP 8



Convenciones de nombres


1. Para declarar variables, funciones, clases, paquetes, etc. utilizar nombres que describan su uso: imprimir_factura, edad, importe, IVA, Cliente

2. Cuando se utilicen nombres cortos de un carácter evitar los caracteres l, I y O para no confundirlos con otros caracteres parecidos: 1, 0.

3. Para los nombres de variables usar una letra, una palabra o varias palabras, en minúsculas. Es recomendable separar las palabras con guiones bajos para mejorar la legibilidad: z, contador, kms_cuadrados.

4. Para los nombres de constantes usar una letra, una palabra o varias palabras, en mayúsculas. Es recomendable separar las palabras con guiones bajos para ganar en legibilidad: E, GRAVEDAD, VELOCIDAD_LUZ.

5. Para los nombres de funciones, de métodos y de módulos usar una palabra o varias palabras, en minúsculas. Para mayor claridad es recomendable separar las palabras con guiones bajos: invertir, aplicar_dto, mi_modulo.py

6. Para los nombres de clases usar una palabra o varias palabras, en minúsculas pero con la inicial en mayúsculas: Usuario,VehiculoDeportivo.

7. Para los nombres de excepciones usar una palabra o varias palabras, en minúsculas pero con la inicial en mayúsculas. Cuando se trate de un error los nombres deberían contener el sufijo "Error": ErrorCalculoFechas

8. Para los nombres de paquetes usar una palabra o varias palabras, en minúsculas: paqcorreo, funconver.

9. El doble guion bajo al principio y final de un nombre está reservado para determinados objetos y atributos con nombres predefinidos (como __name__, __file__, __init__).


Diseño



Líneas de código


10. Las líneas de código deben tener una longitud máxima de 79 caracteres.

11. Las líneas largas que superen la longitud máxima se dividirán en varias líneas utilizando, preferentemente, la continuación de línea implícita que conlleva el uso de paréntesis, corchetes y llaves:

lista1 = ['uno', 'dos', 'tres', 'cuatro', 'cinco',
          'seis', 'siete']

12. En otros casos para dividir una línea que supere el límite debe utilizarse la barra diagonal invertida "\"-

if variable1 > variable2 and variable3 > variable4 \
   or variable1 > variable4:

13. Cuando se trate de operaciones matemáticas extensas otra forma de dividir una línea, que da más claridad al cálculo que se realiza, consiste en situar cada operando en una línea precedido del operador. Desde el punto de vista matemático tiene más sentido leer primero el operador y después la variable a la que afecta.

total = (variable1
         + variable2
         - variable3
         - variable4)


Codificación


14. En Python 3 los archivos de programas que utilicen la codificación UTF-8 no requieren incluir la siguiente declaración al principio del código:

# -*- coding: utf-8 -*-

La mayoría de los editores incorporan una opción para establecer esta codificación por defecto:

  • En el editor Geany la opción se encuentra en el menú Editar, Preferencias, Archivos, Codificaciones, Codificación predeterminada (para los archivos nuevos).
  • En Visual Studio Code la opción está en el menú Archivo, Preferencias, Editor de texto, Archivos, Encoding.

Tanto Geany como Visual Studio Code muestran en la barra de estado la codificación de los archivos fuente con los que se trabaja en un momento dado.


Líneas en blanco


15. Antes y después de la definición de una función o una clase se deben dejar dos líneas en blanco.

def funcion1():
    pass


def funcion2():
    pass


variable1 = 0
variable2 = 1

16. Antes y después de la definición los métodos de una clase dejar una línea en blanco.

class Clase1():
    """clase Clase1"""
    varclase1 = "variable de clase1"

    def metodo1(self, var1):
        self.var1 = var1

    def metodo2(self):
        self.var1 += 1    

17. En funciones y métodos extensos en los que sea posible agrupar el código en bloques que identifiquen diferentes procesos, para facilitar la comprensión dejar una línea en blanco de separación entre ellos. No agregar líneas en blanco al final del código.


Sangría


18. Sangrar el código significa mover un bloque del mismo hacia la derecha insertando espacios o tabuladores para mejorar su legibilidad. Para sangrar el código utilizar preferentemente el carácter del espacio en blanco. Se recomienda utilizar sangrias de 4 espacios en blanco en cada nivel. (Python 3 no permite mezclar sangrías de espacios con las basadas en tabuladores).

if var1 > valor:
    print('Cadena de texto1')
    if not var2:
        print('Cadena de texto2')

19. Cuando una sentencia ocupa más de una línea, el código que sigue en la siguiente línea puede estar alineado con el carácter delimitador de apertura.

lista1 = ['texto1', 'texto2', 'texto3', 
          'texto4', 'texto5', 'texto6']

20. El paréntesis, el corchete y la llave de cierre pueden alinearse bajo el primer carácter del primer elemento de la lista de la línea anterior:

lista1 = [
    elemento1, elemento2,
    elemento3, elemento4
    ]

21. El cierre también puede estar alineado con el primer carácter de la primera línea donde comience la sentencia:

lista1 = [
    elemento1, elemento2,
    elemento3, elemento4
]

22. En declaraciones de funciones o clases que abarcan más de una línea se puede dejar una sangría doble (de 8 espacios) en el código que sigue en la siguiente línea para que se distingan adecuadamente los argumentos:

def funcion(
        valor1, valor2, valor3):
    total = valor1 + valor2 + valor3
    total = total * 25

23. Si se escriben argumentos en la primera línea los que continúan en la siguiente debe estar alineados verticalmente con los primeros:

def funcion(var1, var2, 
            var3):
    total = var1 + var2 + var3
    return(total * 5)

24. En la asignación de una función a una variable los argumentos se escriben agregando una sangría de cuatro espacios. Este tipo de sangría se llama colgante porque en la declaración, que abarca varias líneas, todas las líneas están sangradas excepto la primera. En la primera línea donde aparece el nombre de la función no se incluyen argumentos (o ningún elemento si se tratara de la asignación de una lista, etc.).

datos1 = obtener_datos(
    var1, var2,
    var3, var4)
datos2 = 34

25. Otra posibilidad que contempla PEP 8 cuando se asigna una función a una variable consiste e escribir los argumentos dejando una sangría con un número de espacios menor de 4.

datos1 = obtener_datos(
  var1, var2,
  var3, var4)
datos2 = 34

26. En declaraciones if que ocupan varias lineas la suma de los dos caracteres "if" más el espacio en blanco que le sigue más un paréntesis en la primera línea definen el espacio equivalente a una sangría de 4 espacios para las siguientes líneas. Esta coincidencia puede suponer un conflicto visual cuando finaliza la condición y a partir de la siguiente línea se continúa añadiendo código manteniendo la misma sangría. Se puede optar por no añadir una sangría extra si no afecta esta situación a la legibilidad:

if (condicion1 and condicion2 and
    condicion3 and condicion4):
    var1 = var2

27. Otra posibilidad consiste en insertar un comentario que delimite la declaración if del resto de líneas:

if (condicion1 and condicion2 and
    condicion3 and condicion4):
    # Si todas las condiciones son ciertas se asigna var2 a var1
    var1 = var2

28. O añadir una sangría adicional en la declaración if en la segunda línea:

if (condicion1 and condicion2 and
        condicion3 and condicion4):
    var1 = var2


Comentarios


29. Las líneas que contengan comentarios o cadenas de documentación deben tener una longitud máxima de 72 caracteres.

# Lista de funciones


def funcion1(x, y):
    """
    Función: funcion1
    Devuelve el resultado de la operación x+y
    """
    return x + y


print(funcion1(10, 20))
print(funcion1.__doc__)

30. Los comentarios deben estar construidos con oraciones completas que hay que actualizar cuando el código sufra algún cambio. Un comentario comienza con el carácter # seguido de un espacio en blanco al que sigue el texto comenzando su escritura con mayúsculas y terminando cada oración con un punto. Si el comentario está formado por varias oraciones hay que separarlas con con dos espacios en blanco. Si el comentario es corto no es necesario terminar conun punto.

# Esta es la primera oración.  Y sigue otra separada con dos espacios.

31. Los comentarios de un bloque de código sangrado deben comenzar su escritura manteniendo el mismo nivel de sangrado. Si el comentario está formado por varios párrafos tendremos que dejar entre ellos una línea en blanco que comience con el carácter #.

if condicion1 and condicion2:
    # Primer párrafo del comentario.
    #
    # Segundo párrafo del comentario.
    variable1 = variable2 ** 2
    fucion1(variable1)

32. En líneas que contengan código utilizar con moderación los comentarios. Deben servir para aclarar algún punto no para redundar en lo obvio. Comenzar la escritura del comentario dejando dos espacios en blanco.

variable1 = variable2 + (x * y * z)  # Comentario de línea

33. Una cadena de documentación o docscring es un texto delimitado o entre triples comillas -simples (''') o dobles (""")- situado al principio de un módulo, una clase, una función o un método que sirve para explicar la utilidad del código. En cadenas de documentación multilineas terminar el texto situando la triple comillas de cierre aislada en la línea final. Preferentemente, emplear la triple comillas doble y en docstring de una línea cerrar la cadena en la misma línea. Para más información consultar el documento PEP 257.

def funcion1(param1, param2):
    """Función que devuelve ...

    Texto explicativo detallando procesos...
    Si param1 > param2:
    valor = param1 * param2
    En caso contrario:
    valor = param1 / param2
    
    """
    if param1 > param2:
        total = param1 * param2
    else:
        total = param1 / param2
    
    return total


""" Cadena de documentación de una línea """


Espacios en blanco


34. En asignaciones que utilizan el signo igual (=) o (+=, -=, etc.) agregar un espacio en blanco delante y después del signo. Aplicar la misma recomendación con los operadores de comparación (==, <, >, !=, <>, <=, >=, in, not in, is, is not), con los operadores booleanos (and, or, not) y con las flechas (->) en las anotaciones de las funciones.

var1 = 1
var2 += 1
if var1 == var2:
    var3 = 3
if not var1 and var2 is int:
    print(var1)


def funcion1() -> cadena:
    pass

35. En asignaciones a parámetros de funciones o métodos no añadir espacio en blanco delante y detrás del signo igual (=).

def funcion1(var1=0, var2=0):
    pass

36. Cuando se utilicen operadores con diferentes prioridades, considerar agregar espacios en blanco alrededor de los operadores con las prioridades más bajas. También se puede seguir la misma pauta con declaraciones if donde hay múltiples condiciones.

var1 = 1*var2 + 2/var3
var2 = (x+y) * (x-y)

if var1>0 and var!=10:
    pass

37. En sentencias que utilizan el método [::] que referencia a un subconjunto de elementos de una secuencia (como un grupo de caracteres de una cadena de texto, elementos de una lista, etc) no agregar espacios delante o detrás de los dos puntos (:) a no ser que mejore la legibilidad de una expresión. En referencias a elementos de listas, tuplas y diccionarios no incluir espacios en blanco, ni antes ni después.

tupla1 = (lista1[1:9], lista1[1:9:3], tupla2[1])
var1 = lista1[x+1 : x+y+1]
dicc1['a'] = lista1[0]

38. En expresiones y declaraciones entre paréntesis, corchetes y llaves no agregar inmediatamente después de la apertura o antes del cierre espacios en blanco. Después de una coma añadir un espacio en blanco, excepto entre una coma final y el cierre. No agregar espacios en blanco delante de una coma, punto y coma o dos puntos. No se recomienda dejar espacios delante del paréntesis de apertura que recoge los argumentos de una función. Tampoco después de una declaración al final de la línea, ni entre variables y sus valores para que queden alineados verticalmente.

lista1 = [1, 2, 3, 4, 5, (6, 7)]
tupla2 = ('a', 'b', 'c')
tupla3 = ('a',)
dicc1 = {'a': '1', 'b': '2', 'c': '3'}
var1 = funcion1(var2, var3)

# No se recomienda
variable1 = 1
par1      = True


Otras recomendaciones


39. Aunque la sintaxis de Python lo permita no es recomendable escribir más de una declaración por línea:

# Recomendado
if var1 == 'a':
    hacer()
try:
    print('Hecho')
except ValueError:
    print('Error')

# No recomendado
if var1 == 'a': hacer()
try: hacer()
except: print('error')
finally: finalizar()

40. Se aconseja utilizar comas finales en tuplas de un elemento y en listas que se extiendan por varias líneas, tras cada elemento.

tupla1 = (var1,)
lista1 = (
    'uno.dat',
    'dos.dat',
    'tres.dat',
    )

41. Para asignaciones de cadenas largas utilizar tantas cadenas entre comillas como sea necesario, ocupando cada una de ellas una línea y delimitadas por paréntesis en lugar de cadenas de documentación.

# Recomendado
cadena_larga = (
    "Uno, dos, tres, cuatro, cinco, seis, siete, "
    "ocho, nueve y diez."
)

# No recomendado
cadena_larga = """Uno, dos, tres, cuatro, cinco, seis, siete, \
    ocho, nueve y diez.”"""

42. Para evaluar si una variable booleana tiene el valor True utilizar if var1 en lugar de if var1 == True:

# Recomendado
if var1:
    print('Es verdadero')
   
# No recomendado
if var1 == True:
    print('Es verdadero')

43. Para evaluar si una cadena, lista o tupla están vacías o no tiene elementos utilizar if var1 o if not var1 en lugar de if len(var1) o if not len(var1).

variable1 = ''
lista1 = []
if not variable1 and not lista1:
    print('Tanto la variable como la lista están vacías')

44. Para evaluar si una variable tiene o no un valor definido utilizar if var1 is not None o if var is None en lugar de if var1 != None o if var1 == None.

variable1 = None
if variable1 is None:
    print('La variable no tiene ningún valor definido')

45. Para evaluar si una cadena de texto comienza o termina por otra utilizar los métodos startswith() y endswith() en lugar del método [:].

# Recomendado
cadena = 'Python para impacientes'
if cadena.startswith('Python'):
    print('La cadena de texto comienza con "Python"')

# No recomendado
if cadena[0:6] == 'Python':
    print('La cadena de texto comienza con "Python"')

46. Para operar con cadenas utilizar preferentemente los métodos de cadenas en lugar del módulo string.

47. Para comparar distintos tipos de objetos utilizar isinstance() en lugar de type().

variable1 = 3.5
if isinstance(variable1, (int, float)):
    print('La variable es de tipo numérica')

48. En claúsulas try/except limitar try al mínimo de líneas y usar los nombres de las excepciones para capturar los errores.

var1 = 10
try:
    var1 = var1 / 2
    print(var1)
except ValueError:
    print('Error')


Herramientas para revisar y corregir


49. Es recomendable utilizar un lint para revisar si el estilo de nuestros programas cumple PEP 8. Los más conocidos son pycodestyle y flake8 que están disponibles en el repositorio PyPI. Ambos buscan errores de estilo en un programa y muestran mensajes que ayudan a subsanarlos.

Para instalar pycodestyle:

$ pip install pycodestyle

Para añadir una opción al menú del editor Geany que compruebe con pycodestyle el estilo de un programa: acceder con un archivo .py abierto al menú Construir, Establecer comandos de construcción, y en la línea 3 de Comandos de Python añadir la etiqueta Estilo asociada al siguiente comando y aceptar los cambios:

pycodestyle --max-line-length=79 "%f"


En el menú Construir se ha añadido la opción Estilo para comprobar el estilo de los fuentes cuando sea necesario.

Desde la línea de comandos también es posible verificar el estilo:

$ pycodestyle programa.py

50. Y para arreglar aquellos programas del pasado sin estilo se puede utilizar la herramienta black que reformatea el código aplicando las recomendaciones de PEP 8.

Para instalar black:

$ pip install black

El siguiente programa areas.py no cumple PEP 8:

areas.py:

def area_triangulo(base,altura):
    return base*altura/2

b= 5
h=10
area=area_triangulo(b,h)
print('Área del triángulo:', area)

Para reformatear con black el código de areas.py aplicando las recomendaciones, ejecutar:

$ black --line-length=79 areas.py

black areas.py
reformatted areas.py
All done!
1 file reformatted.

Con posterioridad podemos examinar el archivo areas.py para comprobar los cambios de estilo aplicados:

def area_triangulo(base, altura):
    return base * altura / 2


b = 5
h = 10
area = area_triangulo(b, h)
print("Área del triángulo:", area)

Para consultar información de ayuda de otras opciones de black:

$ black -h




Ir al índice del tutorial de Python

sábado, 24 de agosto de 2019

Rastreando la ejecución de un programa (trace)



El módulo trace permite seguir el flujo, línea a línea, de la ejecución de un programa y generar distintos informes con las líneas que se ejecutan, las que no y el número de veces que se ejecutan. También, rastrea las llamadas a funciones, las relaciones existentes entre los distintos módulos y facilita el análisis de los datos acumulados obtenidos como resultado de varios rastreos de la ejecución de un programa.

Esta herramienta tiene como objeto ayudar a los desarrolladores a mejorar los algoritmos, permitiendo detectar líneas o bloques de código innecesarios y otros errores de lógica. No se debe confundir con un depurador de código pues no permite detener temporalmente el flujo de la ejecución, ni examinar el valor que tienen las variables en un momento dado.


Rastrear la ejecución de un programa


Para mostrar el uso de las opciones más importantes del módulo trace utilizaremos el programa primos.py que obtiene todos los números primos que hay entre dos números dados. En Matemáticas, un número primo es un número natural mayor que 1 que solo es divisible entre sí mismo y 1.

El código de primos.py cuando se ejecuta presenta un mensaje alertando al usuario sobre el tiempo que va a tardar el proceso, que será distinto en función al tamaño del número que determina el límite superior, concretamente, el valor de la variable superior. Después, el programa hace una pausa de 3 segundos y continúa con la obtención y visualización de los números primos agrupados por el número de sus dígitos. Para finalizar, calcula el tiempo que ha durado el proceso y ofrece el total de números primos encontrados.

primos.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pprint
import time


inferior = 1
superior = 10
mostrar_listas_primos = True

long_sup = len(str(superior))
if long_sup > 6:
    print('Échate una siesta...\n\n')
elif long_sup > 5:
    print('Date un paseo corto, de un minuto... \n\n')
else:
    print('No te vayas muy lejos. Termino ya... \n\n')
time.sleep(3)

inicio = time.time()
print("Números primos entre", inferior, "y", superior)
print(75*'-')
contador = 0
cont_long = 0
long_inf = len(str(inferior))
lprimos = []
pp = pprint.PrettyPrinter(width=70, compact=True)
for numero in range(inferior, superior + 1):
    longitud = len(str(numero))
    if numero > 1:
        for divisor in range(2, numero):
            if (numero % divisor) == 0:
                break
        else:
            lprimos.append(numero)
            contador += 1
            cont_long += 1
            
    if longitud != long_inf or numero == superior:
        if mostrar_listas_primos:
            pp.pprint(lprimos)
        print('Total primos con', long_inf, 'dígito/s:', cont_long)   
        print(75*'-')
        long_inf = longitud
        cont_long = 0
        lprimos = []
        
print('Total números primos:', contador)
final = time.time()
tiempo = final - inicio
print(75*'-')
print('El cálculo ha tardado', tiempo, 'segundos' )   


Rastrear las líneas que se ejecutan


El comando siguiente muestra, una a una, las líneas que se ejecutan en el proceso de obtención de los números primos así como las salidas en las que se imprima alguna información.

$ python3 -m trace -t --ignore-dir=/usr/lib/python3.7 primos.py

El argumento -t se utiliza para rastrear las líneas que se ejecutan.

Para limitar el rastreo a las líneas del programa primos.py obviando los módulos importados se agrega el argumento --ignore-dir con la ruta donde se encuentra la biblioteca del intérprete Python ¿Cómo conocer esta ruta? Es fácil, ejecutar el siguiente código Python:

import os, trace
print(os.path.dirname(trace.__file__))

En estas líneas se importan los módulos os y trace y se imprime la ruta del archivo trace.py que se encuentra, entre otros módulos, en dicha biblioteca.

Dependiendo del sistema operativo y de la versión de Python instalada obtendremos rutas similares a las siguientes:

  • /usr/lib/python3.7
  • C:\Python36\lib
  • C:\Users\user1\AppData\Local\Programs\Python\Python37\lib

Continuando con el comando del rastreo, como puede apreciarse en la salida resultante solo aparecen las líneas ejecutadas que muestran el flujo que ha seguido el programa desde principio a fin. Se omiten las líneas no ejecutadas, las primeras por tratarse de comentarios y aquellas que están en blanco. Si la ejecución entra en un bucle, en la salida obtenida aparecerán las líneas tantas veces como se ejecuten, hasta salir del bucle:

Salida:

 --- modulename: primos, funcname: 
primos.py(4): import pprint
primos.py(5): import time
primos.py(8): inferior = 1
primos.py(9): superior = 10
primos.py(10): mostrar_listas_primos = True
primos.py(12): long_sup = len(str(superior))
primos.py(13): if long_sup > 6:
primos.py(15): elif long_sup > 5:
primos.py(18):     print('No te vayas muy lejos. Termino ya...\n\n')
No te vayas muy lejos. Termino ya... 


primos.py(19): time.sleep(3)
primos.py(21): inicio = time.time()
primos.py(22): print("Números primos entre", inferior, "y",superior)
Números primos entre 1 y 10
primos.py(23): print(75*'-')
--------------------------------------------------------------------
primos.py(24): contador = 0
primos.py(25): cont_long = 0
primos.py(26): long_inf = len(str(inferior))
primos.py(27): lprimos = []
primos.py(28): pp = pprint.PrettyPrinter(width=70, compact=True)
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(36):             lprimos.append(numero)
primos.py(37):             contador += 1
primos.py(38):             cont_long += 1
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(36):             lprimos.append(numero)
primos.py(37):             contador += 1
primos.py(38):             cont_long += 1
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(34):                 break
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(36):             lprimos.append(numero)
primos.py(37):             contador += 1
primos.py(38):             cont_long += 1
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(34):                 break
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(36):             lprimos.append(numero)
primos.py(37):             contador += 1
primos.py(38):             cont_long += 1
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(34):                 break
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(34):                 break
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(30):     longitud = len(str(numero))
primos.py(31):     if numero > 1:
primos.py(32):         for divisor in range(2, numero):
primos.py(33):             if (numero % divisor) == 0:
primos.py(34):                 break
primos.py(40):     if longitud != long_inf or numero == superior:
primos.py(41):         if mostrar_listas_primos:
primos.py(42):             pp.pprint(lprimos)
[2, 3, 5, 7]
primos.py(43):         print('Con', long_inf, 'díg./s:', cont_long)   
Total primos con 1 dígito/s: 4
primos.py(44):         print(75*'-')
--------------------------------------------------------------------
primos.py(45):         long_inf = longitud
primos.py(46):         cont_long = 0
primos.py(47):         lprimos = []
primos.py(29): for numero in range(inferior, superior + 1):
primos.py(49): print('Total números primos:', contador)
Total números primos: 4
primos.py(50): final = time.time()
primos.py(51): tiempo = final - inicio
primos.py(52): print(75*'-')
--------------------------------------------------------------------
primos.py(53): print('El cálculo ha tardado', tiempo, 'segundos' )       
El cálculo ha tardado 0.0014536380767822266 segundos

También, como alternativa a omitir el rastreo de la biblioteca Python, se puede omitir el rastreo de uno o más módulos con el argumento --ignore-module:

$ python3 -m trace -t --ignore-module=_bootstrap,_bootstrap_external primos.py

En este caso además de rastrear las líneas ejecutadas del programa primos.py se traza la ejecución de las líneas ejecutadas del módulo pprint. Las funciones time() o sleep() del módulo time no aparecen porque la mayoría de las funciones definidas en este módulo llaman a funciones de la librería de C.


Rastrear, contar líneas y obtener líneas no ejecutadas


Para rastrear las líneas ejecutadas, las no ejecutadas y almacenar en un archivo cover un informe con el código con el número de veces que se ejecuta cada línea:

$ python3 -m trace --count -C . -t -m --ignore-dir=/usr/lib/python3.7 primos.py

El argumento --count indica al trazador que debe contar el número de veces que se ejecutan las líneas en el programa y en cualquier módulo que se utilice que no se haya excluido. El total de ejecuciones aparece precediendo al código de cada línea.

El argumento -C (--converdir) establece el directorio donde se crean los informes (con un punto . se indica que el directorio es el mismo donde se encuentre el programa o el módulo ejecutado).

El argumento -m (--missing) se utiliza para recoger las líneas que no se ejecutan que aparecen señaladas con la cadena ">>>>>>". Esta opción resulta de mucha utilidad para identificar posibles líneas de código innecesarias.

En este caso se obtiene la misma salida por pantalla que en el primer ejemplo pero además se genera un archivo llamado primos.cover con el siguiente contenido:

primos.cover:

       #!/usr/bin/env python
       # -*- coding: utf-8 -*-
       
    1: import pprint
    1: import time
       
       
    1: inferior = 1
    1: superior = 10
    1: mostrar_listas_primos = True
       
    1: long_sup = len(str(superior))
    1: if long_sup > 6:
>>>>>>     print('Échate una siesta...\n\n')
    1: elif long_sup > 5:
>>>>>>     print('Date un paseo corto, de un minuto... \n\n')
       else:
    1:     print('No te vayas muy lejos. Termino ya... \n\n')
    1: time.sleep(5)
       
    1: inicio = time.time()
    1: print("Números primos entre", inferior, "y", superior)
    1: print(75*'-')
    1: contador = 0
    1: cont_long = 0
    1: long_inf = len(str(inferior))
    1: lprimos = []
    1: pp = pprint.PrettyPrinter(width=70, compact=True)
   11: for numero in range(inferior, superior + 1):
   10:     longitud = len(str(numero))
   10:     if numero > 1:
   19:         for divisor in range(2, numero):
   15:             if (numero % divisor) == 0:
    5:                 break
               else:
    4:             lprimos.append(numero)
    4:             contador += 1
    4:             cont_long += 1
                   
   10:     if longitud != long_inf or numero == superior:
    1:         if mostrar_listas_primos:
    1:             pp.pprint(lprimos)
    1:         print('T primos con', long_inf, 'díg./s:', cont_long)   
    1:         print(75*'-')
    1:         long_inf = longitud
    1:         cont_long = 0
    1:         lprimos = []
               
    1: print('Total números primos:', contador)
    1: final = time.time()
    1: tiempo = final - inicio
    1: print(75*'-')
    1: print('El cálculo ha tardado', tiempo, 'segundos' )    


Rastrear llamadas a funciones


El argumento --listfuncs que no es compatible con rastrear y contar las líneas ejecutadas (opciones -t y -c, respectivamente) muestra una lista con las funciones que han sido llamadas durante la ejecución.

$ python3 -m trace --listfuncs primos.py

Extracto de la salida:

No te vayas muy lejos. Termino ya... 


Números primos entre 1 y 10
--------------------------------------------------------------------
[2, 3, 5, 7]
Total primos con 1 dígito/s: 4
--------------------------------------------------------------------
Total números primos: 4
--------------------------------------------------------------------
El cálculo ha tardado 0.013412952423095703 segundos

functions called:
filename: .../pprint.py, modulename: pprint, funcname: 
filename: .../pprint.py, modulename: pprint, funcname: PrettyPrinter

...
...


Rastrear relaciones entre módulos por llamadas a funciones


El argumento --trackcalls muestra las relaciones existentes entre los distintos módulos por las llamadas a funciones efectuadas durante la ejecución, ampliando los detalles del comando anterior:

$ python3 -m trace --listfuncs --trackcalls primos.py

Extracto de la salida:

No te vayas muy lejos. Termino ya... 


Números primos entre 1 y 10
--------------------------------------------------------------------
[2, 3, 5, 7]
Total primos con 1 dígito/s: 4
--------------------------------------------------------------------
Total números primos: 4
--------------------------------------------------------------------
El cálculo ha tardado 0.010804176330566406 segundos

calling relationships:

*** /usr/lib/python3.7/pprint.py ***
    pprint. -> pprint.PrettyPrinter
    pprint. -> pprint._safe_key
  --> 
    pprint. ->  pprint.PrettyPrinter._repr
    pprint.PrettyPrinter._repr -> pprint.PrettyPrinter.format
    pprint.PrettyPrinter.format -> pprint._safe_repr
    pprint.PrettyPrinter.pprint -> pprint.PrettyPrinter._format
    pprint._safe_repr -> pprint._safe_repr

*** /usr/lib/python3.7/trace.py ***
    trace.Trace.runctx -> trace._unsettrace
  --> primos.py
    trace.Trace.runctx -> primos.

...
...


Rastrear y obtener informes combinados


El argumento -r (--report) genera una lista anotada a partir de uno o varios rastreos anteriores del programa en los que se utilizan las opciones -c (--count) y -f (--file). Este argumento por si solo no ejecuta ningún código, simplemente acumula los datos obtenidos en distintos rastreos en los que es habitual que se modifiquen antes los valor de variables para conocer el comportamiento del código en situaciones diferentes.

Para mostrar el funcionamiento con el programa primos.py trazaremos el código tres veces modificando antes el valor de la variable superior y después obtendremos un informe combinado. En el primer rastreo la variable superior tendrá el valor 10; en el segundo, 100 y en el tercero y último, 1000.

En este caso crear en la ubicación del programa primos.py un directorio llamado inf para guardar en esa ubicación los archivos cover y el archivo que acumulará los datos inf.dat del argumento -f (--file):

# En Windows:

C:\> md inf

# En GNU/Linux:

$ mkdir inf

A continuación, antes de cada rastreo cambiar el valor de la variable superior. En el primer rastreo se produce el error [Errno 2] porque no existe todavía el archivo inf.dat. Es normal. Después de su ejecución en la carpeta inf se almacenan los archivos .cover y en pantalla se produce la salida con el flujo de la ejecución. También, hay que tener en cuenta que como no se excluye del rastreo el directorio de la biblioteca de Python, se obtienen tres archivos .cover (primos.cover, pprint.cover y trace.cover) además del acumulado inf.dat:

# Rastreo 1 con variable superior = 10:

$ python3 -m trace --count -C inf -f inf/inf.dat -t primos.py


# Rastreo 2 con variable superior = 100:

$ python3 -m trace --count -C inf -f inf/inf.dat -t primos.py


# Rastreo 3 con variable superior = 1000:

$ python3 -m trace --count -C inf -f inf/inf.dat -t primos.py


# Generar informe combinado:

$ python3 -m trace -C inf -r -s -m -f inf/inf.dat -t primos.py

El comando anterior actualiza los archivos .cover acumulando sus datos y muestra en la salida de pantalla un resumen -s (--summary) con el número de líneas ejecutadas de cada módulo y el porcentaje que representan estas líneas con respecto al total, después de los tres rastreos:

Salida:

lines   cov%   module   (path)
  448    36%   pprint   (/usr/lib/python3.7/pprint.py)
   42    95%   primos   (primos.py)
  457     0%   trace    (/usr/lib/python3.7/trace.py)

Como puede observarse en la salida, el programa primos.py muestra un 95% del código utilizado. ¿De este valor se podría deducir que un 5% del código es desechable?. Bueno, para responder a esta pregunta es necesario analizar las líneas no ejecutadas en el archivo primos.cover, que aparecen porque se incluyó el argumento -m (--missing) en el comando anterior:

       #!/usr/bin/env python
       # -*- coding: utf-8 -*-
       
    3: import pprint
    3: import time
       
       
    3: inferior = 1
    3: superior = 10
    3: mostrar_listas_primos = True
       
    3: long_sup = len(str(superior))
    3: if long_sup > 6:
>>>>>>     print('Échate una siesta...\n\n')
    3: elif long_sup > 5:
>>>>>>     print('Date un paseo corto, de un minuto... \n\n')
       else:
    3:     print('No te vayas muy lejos. Termino ya... \n\n')
    3: time.sleep(5)
       
    3: inicio = time.time()
    3: print("Números primos entre", inferior, "y", superior)
    3: print(75*'-')
    3: contador = 0
    3: cont_long = 0
    3: long_inf = len(str(inferior))
    3: lprimos = []
    3: pp = pprint.PrettyPrinter(width=70, compact=True)
 1113: for numero in range(inferior, superior + 1):
 1110:     longitud = len(str(numero))
 1110:     if numero > 1:
79367:         for divisor in range(2, numero):
79170:             if (numero % divisor) == 0:
  910:                 break
               else:
  197:             lprimos.append(numero)
  197:             contador += 1
  197:             cont_long += 1
                   
 1110:     if longitud != long_inf or numero == superior:
    6:         if mostrar_listas_primos:
    6:             pp.pprint(lprimos)
    6:         print('T primos con', long_inf, 'díg./s:', cont_long)   
    6:         print(75*'-')
    6:         long_inf = longitud
    6:         cont_long = 0
    6:         lprimos = []
               
    3: print('Total números primos:', contador)
    3: final = time.time()
    3: tiempo = final - inicio
    3: print(75*'-')
    3: print('El cálculo ha tardado', tiempo, 'segundos' )    

Con respecto a las líneas no ejecutadas (las precedidas con >>>>>>) se puede concluir que son necesarias para otros casos en los que el valor de la variable superior sea mayor a los valores que hemos usado en el ejemplo.

También, como el rastreo del programa se realizó tres veces, algunas líneas muestran que se han ejecutado ese número de veces aunque, lógicamente, todas acumulan igualmente el número total de ejecuciones gracias al argumento --count.


Agregar opción para rastrear en el editor de código


Para finalizar hacemos la sugerencia de agregar una opción "Rastrear" al editor de código fuente, siempre que éste lo permita, para hacer del uso de trace algo habitual.

En Geany, editor de referencia de este blog, se puede agregar una opción que ejecute el comando de rasreo cuando sea necesario. Para ello, crear un nuevo documento Python o abrir alguno existente y acceder al menú Construir, Establecer comandos de construcción. Después, en el apartado "Comandos de ejecución" de la nueva ventana agregar en el espacio reservado para el comando número 2 (si no se usa para otra tarea) la etiqueta "Rastrear" asociada al comando siguiente y aceptar los cambios:

python3 -m trace --ignore-dir=/usr/lib/python3.7 --trace "%f"



Para concluir apuntar que el módulo trace se puede utilizar también desde un programa. Para conocer los detalles y consultar más opciones recomendamos consultar la documentación oficial de Python.

Relacionado:


Ir al índice del tutorial de Python

martes, 9 de julio de 2019

Facilitando la depuración de programas con breakpoint()




Python 3.7 incluye una mejora que facilita a los desarrolladores el acceso al depurador de programas de Python (Pdb). Antes para poder invocar al depurador era necesario incluir en un programa la siguiente línea:

import pdb; pdb.set_trace()

Esto ya no es necesario. Ahora para depurar el código simplemente tendremos que insertar la función breakpoint() en el lugar o lugares donde se quieran establecer los puntos de interrupción. Con este cambio se persigue facilitar la tarea, hacerla más cómoda e intuitiva.

Después, una vez iniciado el proceso de depuración todo funciona como hasta ahora: cuando se alcanza un punto de interrupción se hace una parada temporal y se devuelve el control al usuario para ejecutar el programa paso a paso (n) o hasta el siguiente punto de interrupción (c), consultar el valor de las variables, etc. Veamos un caso práctico.

El siguiente ejemplo contiene una lista de valores a sumar. Para ello, se recorren, uno a uno, los distintos elementos y se van sumando a la variable acumula, que inicialmente tiene el valor 0. El programa sumando.py genera una excepción cuando se alcanza el cuarto elemento ('a') por ser de tipo cadena; y por no poder sumarse a un valor numérico. En este caso para la depuración se incluye el punto de interrupción justo antes de la suma del elemento en curso para permitir evaluar su valor.

sumando.py:

lista = [2, 1, 3, 'a', 4]
acumula = 0
for valor in lista:
    breakpoint()
    acumula+=valor
    print(acumula)

Para iniciar la depuración, ejecutar:

$ python3 sumando.py

Una vez iniciada, el programa avanzará hasta el punto de interrupción. Después, si escribimos el nombre de alguna de las variables (valor o acumula) y presionamos [return] obtendremos su valor actual. A continuación, al pulsar 'c' y [return] el programa mostrará el resultado de la primera suma y completará un ciclo hasta alcanzar de nuevo el punto de interrupción. Si repetimos la acción y vamos consultando la variable valor observaremos que el error se produce cuando se alcanza el cuarto elemento de la lista:

TypeError: unsupported operand type(s) for +=: 'int' and 'str'

Para obtener ayuda de otras opciones del depurador escribir 'h' y [return]. Para ampliar la información de ayuda de alguna opción escribir: 'h' [opción] y [return]. Y para salir del depurador escribir 'exit', 'quit' o 'q' y [return].

Para ejecutar el programa omitiendo la depuración:

$ PYTHONBREAKPOINT=0 python3 sumando.py

Y para ejecutar el programa con el depurador de consola PuDB:

$ PYTHONBREAKPOINT=pudb.set_trace python3 sumando.py


Relacionado:


Ir al índice del tutorial de Python

domingo, 7 de julio de 2019

El módulo array frente a las listas Python



El módulo array de la librería estándar de Python permite declarar un objeto que es similar a una lista pero sólo puede almacenar datos del mismo tipo: números enteros con distintos tamaños y números de punto flotante, entre otros.

Por esta razón cuando se define un array lo primero que hay que establecer es el tipo de dato que va a contener con una letra identificativa. Esto que parece una restricción garantiza también, cuando se trabaja con miles o millones de datos, que todos sean del mismo tipo.

Tipos de Arrays

b - char con signo (int, 1 byte)
B - char sin signo (int, 1 byte)
h - short con signo (int, 2 bytes)
H - short sin signo (int, 2 bytes)
i - int con signo (int, 4 bytes)
I - int sin signo (int, 4 bytes)
l - long con signo (int, 8 bytes)
L - long sin signo (int, 8 bytes)
f - float (float, 4 bytes)
d - double (float, 8 bytes)


Las listas en cambio son estructuras flexibles que pueden contener datos elementales de distintos tipos y otras estructuras como diccionarios, tuplas o incluso otras listas. Además, como las listas son objetos que forman parte del propio lenguaje, en general, será más rápido añadir y procesar elementos en una lista que en un array. El inconveniente lo vamos a encontrar cuando trabajemos con listas de gran tamaño. Los arrays son mucho más eficientes en este sentido y aprovechan mejor los recursos, en especial, la memoria.

La versatilidad de una lista penaliza a medida que su tamaño va creciendo pero sólo cuando se trata de listas muy grandes. En las pruebas que hemos realizado para poner al límite ambas estructuras hemos observado que agregar elementos a una lista era hasta un 40% más rápido que a un array. Este comportamiento se ha mantenido en el equipo que hemos utilizado para las pruebas hasta alcanzar 650.000.000 elementos, aproximadamente. A partir de ahí, a medida que se iban agotando los recursos, el sistema enlentecía drásticamente y con casi 800.000.000 elementos se interrumpía la ejecución del script produciendo un error de memoria (MemoryError) y alguna vez el extraño "Killed". En el caso de los arrays como son estructuras más compactas y seguras hemos podido declarar y operar arrays de 5.000.000.000 elementos sin que el sistema se rompiera.

Por lo demás, tanto las listas como los arrays tienen métodos básicos para agregar, insertar, extender, acceder, borrar, contar elementos, etc. La clase array incorpora los métodos fromfile() y tofile() que permiten leer de un fichero o escribir a un fichero los elementos de un array; aunque para operaciones más complejas y arrays con más de una dimensión lo recomendable es la biblioteca Numpy.

A continuación mostramos el uso del módulo array con algunos ejemplos que servirán para constatar el parecido con las listas, exceptuando el modo en que se declaran y que hay algunos métodos diferentes. 


Declarar arrays



# Importar módulo array
import array

# Declarar lista con valores numéricos
lista1 = [1, 0, 1, 0, 1, 1, 0, 0]

# Obtener cadena con todos los tipos de arrays disponbiles
array.typecodes  # 'bBuhHiIlLqQfd'

# Declarar 'array1' de tipo 'char sin signo' con datos de 'lista1'
array1 = array.array('B', lista1)

# Declarar 'array2' de tipo 'float' con datos de 'lista1'
array2 = array.array('f', lista1)

# Declarar 'array3' de tipo 'int sin signo' con datos de 
# 'lista1' en orden inverso 
array3 = array.array('I', (elemento for elemento in lista1[::-1]))

# Declarar 'array4' de tipo 'char con signo' a partir cadena de bytes
cadena = b'Python'
array4 = array.array('b', cadena)

# Declarar 'array5' de tipo 'int con signo' con valores del 0 al 9
array5 = array.array('i', range(10))

# Obtener tipo de array y datos de 'array1'
array1  # array('B', [1, 0, 1, 0, 1, 1, 0, 0])

# Obtener tipo de array y datos de 'array2'
array2  # array('f', [1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0])

# Obtener tipo de array y datos de 'array3'
array3  # array('I', [0, 0, 1, 1, 0, 1, 0, 1])

# Obtener tipo de array y datos de 'array4'
array4  # array('b', [80, 121, 116, 104, 111, 110])

# Obtener tipo de array y datos de 'array4'
array5  # array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# Obtener tipo de objeto de 'array1'
type(array1)  # array.array

# Obtener tipo de array de 'array1', 'array2' y 'array2'
array1.typecode  # 'B'
array2.typecode  # 'f'
array3.typecode  # 'I'

# Obtener tamaño (en bytes) que ocupa un elemento en los arrays
array1.itemsize  # 1
array2.itemsize  # 4
array3.itemsize  # 4


Operaciones básicas con arrays



# Añadir nuevos elementos a 'array1' y 'array2'
array1.append(1)
array1.append(0)
array2.append(1)

# Obtener número de elementos de 'array1', 'array2' y 'array3'
len(array1)  # 10
len(array2)  # 9
len(array3)  # 8

# Obtener tupla con dirección de memoria actual de los arrays y
# número de elementos (Útil sólo para operaciones de bajo nivel).
array1.buffer_info()  # (140391309074144, 10)
array2.buffer_info()  # (140391299217200, 9)
array3.buffer_info()  # (140391307750896, 8)

# Obtener el número de veces que se repite un valor en un array
array1.count(1)  # 5
array2.count(1)  # 5
array3.count(0)  # 4

# Obtener posición donde aparece por primera vez elemento buscado
array1.index(1)  # 0
array2.index(1)  # 0
array3.index(1)  # 2

# Insertar un elemento delante de la posición indicada en 'array1'
array1.insert(0, 10)
array1.insert(2, 20)
array1  # array('B', [10, 1, 20, 0, 1, 0, 1, 1, 0, 0, 1, 0])

# Borrar el último y tercer elemento en 'array1'
array1.pop()
array1.pop(2)
array1  # array('B', [10, 1, 0, 1, 0, 1, 1, 0, 0, 1])

# Borrar el primer elemento en 'array1' que coincida con el indicado

array1.remove(10)
array1  # array('B', [1, 0, 1, 0, 1, 1, 0, 0, 1])

# Invertir el orden de los elementos en 'array1'
array1.reverse()
array1  # array('B', [1, 0, 0, 1, 1, 0, 1, 0, 1])

# Extender o añadir elementos a un array
array3  # array('I', [0, 0, 1, 1, 0, 1, 0, 1])
array3.extend([0, 0])
array3  # array('I', [0, 0, 1, 1, 0, 1, 0, 1, 0, 0])

# Obtener el primer, tercer y último elemento de 'array3'
array3[0]  # 0
array3[2]  # 1
array3[-1]  # 0

# Cambiar el valor a 1 del primer y penúltimo elemento de 'array3'
array3[0] = 1
array3[-2] = 1


Cadenas de bytes. Lectura/escritura de arrays de/a archivos



# Asignar con frombytes() una cadena de bytes a 'array1' 
array1 = array.array('b')
cadena = b'Arrays Python'
array1.frombytes(cadena)
array1  #  array('b', [65, 114, 114, 97, 121, 115, 32, 80, 121,
        #              116, 104, 111, 110])

# Escribir el contenido de 'array1' a un archivo
cadena = b'Arrays Python'
array1 = array.array('b', cadena)
archivo = open('datos.txt', 'wb')
array1.tofile(archivo)
archivo.close

# Leer y añadir a 'array1' los 6 primeros bytes del archivo anterior
array1 = array.array('b')
archivo = open('datos.txt', 'rb')
array1.fromfile(archivo, 6)
array1  # array('b', [65, 114, 114, 97, 121, 115])

# Convertir a bytes los valores leídos con anterioridad
cadena = array1.tobytes()
cadena  # b'Arrays'

# Convertir bytes a cadena de texto
cadena = cadena.decode()
cadena  # 'Arrays'

# Intentar añadir elementos de una lista a un array que
# contiene elementos. Cuando se produce un error de tipo
# el array mantiene sus datos originales.
lista1 = [0, 1, 3.31, 3.5, 1.9, 0, False]
array1 = array.array('b', [9, 10, 11])
try:
    array1.fromlist(lista1)
except:
    print('Error al intentar agregar lista1')
finally:
    print(array1)

Relacionado:

Ir al índice del tutorial de Python

viernes, 31 de mayo de 2019

Proyectos de documentación con MkDocs



MkDocs es un generador de sitios estáticos que está orientado a la creación de proyectos de documentación. Los documentos se escriben en formato Markdown y el sitio se configura mediante un único archivo de configuración YAML. Es tan fácil de usar que parece pensado para motivar a aquellas personas que son reacias a documentar.

A partir de la documentación este magnífico generador permite crear sitios estáticos HTML que se pueden alojar en un servidor web.

Para configurar la apariencia de un sitio se puede configurar un tema o crear uno original que se adapte a una imagen corporativa o a un diseño propio.

MkDocs incorpora un servidor de desarrollo que permite obtener una vista previa de la documentación a medida que se va creando. Dicha documentación se carga y actualiza automáticamente en el navegador web cada vez que cambia.

Tanto en el servidor de desarrollo como en el sitio estático HTML que se genera se incluye un buscador que hará las delicias de los usuarios.


Instalación de MkDocs


MkDocs se puede instalar con el gestor de paquetes del sistema operativo (apt, yum, etc.), o bien, si el gestor no cuenta con un paquete específico, con el instalador pip, con Python 2.x/3.x

Para instalar MkDocs con el gestor de paquetes apt ejecutar en la consola:

$ sudo apt install mkdocs

Para instalar MkDocs con el instalador pip:

$ pip install mkdocs

Para conocer la versión instalada de MkDocs:

$ mkdocs --version

Para obtener una lista completa de todos los comandos:

$ mkdocs --help


Creando un proyecto nuevo de documentación


Un proyecto de documentación en MkDocs es un conjunto de documentos en formato MarkDown (.md) accesibles desde una opción del menú, desde enlaces que incluyen los propios documentos o desde el buscador de documentos que incorpora el propio sistema.

Los documentos de un proyecto pueden incluir imágenes, enlaces a otros tipos de archivos y enlaces tanto a páginas del propio proyecto como externas.

Para crear un proyecto de documentación ejecutar en la consola:

$ mkdocs new nombre-proyecto

Durante el proceso de creación se mostrará en pantalla la siguiente información:

INFO - Creating project directory: nombre-proyecto 
INFO - Writing config file: nombre-proyecto/mkdocs.yml
INFO - Writing initial docs: nombre-proyecto/docs/index.md

Si todo ha ido bien el comando creará una carpeta con el nombre del proyecto con un archivo de configuración llamado mkdocs.ylm y una carpeta llamada docs para los documentos que se irán agregando al proyecto; que en primera instancia incluye un primer documento llamado index.md con un contenido de ejemplo.

Para acceder al directorio del proyecto:

$ cd nombre-proyecto


Examinando los documentos iniciales


Cuando se crea un proyecto el archivo de configuración mkdocs.ylm sólo contiene una línea con la opción site_name para el nombre del sitio:

site_name: My Docs

Por otro lado, el contenido del archivo index.md, que se encuentra en el directorio docs, está escrito con Markdown. Este archivo incluye un título con un texto de bienvenida al que sigue un enlace a la documentación oficial del proyecto Mkdocs; un título de segundo nivel con el literal "Commands" al que sigue la lista básica de comandos de MkDocs y, finalmente, un apartado con el título de segundo nivel "Project Layout" que muestra la estructura básica de carpetas y archivos de un proyecto de documentación:

# Welcome to MkDocs

For full documentation visit [mkdocs.org](https://mkdocs.org).

## Commands

* `mkdocs new [dir-name]` - Create a new project.
* `mkdocs serve` - Start the live-reloading docs server.
* `mkdocs build` - Build the documentation site.
* `mkdocs help` - Print this help message.

## Project layout

    mkdocs.yml    # The configuration file.
    docs/
        index.md  # The documentation homepage.
        ...       # Other markdown pages, images and other files.

Este contenido será sustituido por el que se quiera incluir en la primera página de un proyecto de documentación.


Comenzando a trabajar con el servidor de desarrollo


MkDocs incorpora un servidor de desarrollo integrado que permite obtener una vista previa de la documentación en un navegador Internet mientras se trabaja.

Para iniciar el servidor es necesario encontrarse en el directorio del archivo de configuración mkdocs.yml y, después, ejecutar el siguiente comando:

$ mkdocs serve

Durante el proceso de inicio se muestra en la consola la siguiente información:

INFO - Building documentation... 
INFO - Cleaning site directory 
[I 190531 10:59:18 server:298] Serving on http://127.0.0.1:8000 
[I 190531 10:59:18 handlers:59] Start watching changes 
[I 190531 10:59:18 handlers:61] Start detecting changes 
[I 190531 10:59:20 handlers:132] Browser Connected ...

A continuación, para mostrar la página de inicio del proyecto hay que acceder a la siguiente dirección http://127.0.0.1:8000/ desde un navegador Internet.

Aspecto inicial de un proyecto

El navegador mostrará en la parte superior una barra que, de momento, sólo incluirá el nombre del sitio obtenido del fichero de configuración y un buscador. Más adelante, haremos mención al modo de incluir un menú de navegación que permita acceder a otros documentos o contenidos del proyecto.

A continuación, se cargará la primera página del proyecto (index.md) con su contenido dividido en dos áreas:

  • En el área de la izquierda se muestra una lista de opciones construida con las líneas de títulos y subtítulos que existen en el documento index.md (son las que tienen al comienzo #, ##,...).
  • Y en el área derecha se muestra el texto del documento debidamente formateado.

El servidor de desarrollo carga de forma automática la documentación cada vez que cambia el archivo de configuración o el contenido de los archivos de la documentación. Esto es fácil de comprobar con la página cargada en el navegador, modificando o añadiendo algún texto y guardando los cambios efectuados. Dichos cambios serán aplicados de forma inmediata en el navegador.

Para detener el servidor presionar en la consola la combinación de teclas [Control+C]

Para iniciar el servidor estableciendo una IP y un puerto diferentes:

$ mkdocs serve -a Dirección-IP:Número-Puerto

Para obtener información de ayuda de otras opciones disponibles:

$ mkdocs serve --help


La barra de navegación


En la barra de navegación de la parte superior de un proyecto se puede incluir un menú de opciones para facilitar el acceso a la documentación. La definición de este menú se hace incluyendo la opción nav en el archivo de configuración mkdocs.yml. Las opciones de un menú pueden tener varios niveles y cada opción final puede cargar un documento del proyecto o un contenido externo.

Si no se ha configurado ningún menú y MkDocs detecta que se han agregado nuevos documentos a la carpeta docs construirá su propio menú basándose en el texto de las líneas de títulos que tengan dichos documentos. También, incluirá opciones para navegar al documento siguiente y al previo.

Lo razonable es crear otras carpetas dentro de la carpeta docs para agrupar documentos con el mismo tipo de contenido. También, es recomendable crear una carpeta para las imágenes (img) y otras que sirvan para localizar archivos del mismo tipo que interesen estar localizados (como archivos .PDF); y que se van a enlazar desde las distintas opciones del menú o desde otros documentos. Establecer cierto orden siempre ayuda a agilizar el trabajo.

A continuación un ejemplo de archivo de configuración con un menú con distintos niveles de opciones que enlazan documentos MarkDown y un PDF:

site_name: Mi proyecto
nav:
- Inicio: 'index.md'
- Tutorial:
    - 'Instalación': 
      - 'Introducción': tutorial/introduccion.md
      - 'Escenarios': tutorial/escenarios.md
      - 'Instalación': tutorial/instalacion.md
    - 'Guía':
      - 'Primeros Pasos': tutorial/primeros_pasos.md
      - 'Referencia': tutorial/referencia.md
      - 'Descargar PDF': PDF/guia.pdf
- Ayuda:
    - 'Contactar': ayuda/contactar.md
    - 'Comunidades': ayuda/comunidades.md
    - 'Acerca de': ayuda/acercade.md

Como puede observarse todas las rutas de los documentos que se indican son relativas. Así deben expresarse también cuando se incluyan en los propios documentos.

También, cuando se define un menú hay que tener en cuenta que cualquier página que exista en la carpeta docs que no se incluya en alguna de las opciones del menú quedará oculta para el usuario, a no ser que sea enlazada desde algún documento.

Para mostrar el menú de opciones del ejemplo, copiar el código al archivo de configuración de un proyecto e iniciar el servidor de desarrollo. Para que los enlaces a los documentos carguen algún contenido crear las carpetas y los documentos necesarios con algún texto de ejemplo. Es posible generar textos aleatorios para pruebas en el sitio Lorem Ipsum.

Buscador

Por último, la barra superior incluye un buscador que permite acceder de forma rápida a los documentos de un proyecto. El buscador presentará un resumen con enlaces a los documentos o secciones que contengan el texto buscado, a medida que se escribe.

Cambiando la apariencia del proyecto


Para cambiar la apariencia del proyecto lo más rápido es incluir en el archivo de configuración la opción theme con el nombre de alguno de los temas disponibles (mkdocs, readthedocs, windmill, etc).

Proyecto basado en el tema MkDocs

Proyecto basado en el tema Windmill

MkDocs permite también desarrollar un tema propio. Hay información relacionada con la personalización de los proyectos en el sitio oficial.

MkDocs también adapta las páginas de un proyecto a la pantalla del dispositivo que se esté utilizando en cada caso:



Finalmente, comentar que se puede sustituir el icono predeterminado del proyecto por otro personalizado (favicon.ico) copiando el icono a la carpeta de imágenes (img) del proyecto.


Construyendo el sitio


Una vez terminada la documentación de un proyecto se puede construir el sitio web correspondiente ejecutando en la consola el siguiente comando:

$ mkdocs build

El proceso de construcción consiste en crear un directorio llamado site con todos los archivos del sitio, entre los que se incluyen los archivos HTML con la documentación creada en dicho formato. Todo el sitio podrá alojarse en un servidor web o en páginas de proyectos como GitHub y Amazon S3.

Para construir el sitio eliminando documentos obsoletos que han sido retirados de la documentación, ejecutar:

$ mkdocs build --clean

Para obtener información de ayuda de otras opciones disponibles:

$ mkdocs build --help

Finalizamos esta introducción animándoles a visitar el sitio oficial para conocer el resto de opciones de configuración que tiene está fantástica herramienta y para profundizar en su conocimiento.


Ir al índice del tutorial de Python