Ecosistema Python

En la clase pasada hicimos un pequeño resumen de Python, pero ahora pondremos énfasis en su ecosistema y comunidad. Lo primoero, Python es un lenguaje de programación interpretado, cuya filosofía hace hincapié en una sintaxis muy limpia y un código legible. Aquí les presento una lista de razones para aprender Python.

  • Código abierto

  • Multiparadigma y Multiplataforma

  • Versatilidad a la hora de programar

  • Sintaxis amigable

Comunidad

Desde 2012, Python ha crecido constantemente en popularidad, y es probable que la tendencia siga aumentando hasta que la comunidad migre a otro lenguaje.Según Stack Overflow Developer Survey 2020 se encuentra en los primeros lugares entre los lenguajes de programación más amados y buscados.

Gracias a la popularidad de Python, es probable que encuentres una solución lista para usar en múltiples problemas que puedas estar enfrentando. La comunidad de Python es grande y están trabajando incansablemente para mejorar el lenguaje todos los días.

Python también tiene una serie de patrocinadores corporativos, presionando para popularizar aún más el lenguaje. Entre ellos se encuentran gigantes tecnológicos como Google o NumFocus.

Ecosistema Científico

Una gran ventaja de Python es la amplia selección de bibliotecas y frameworks que ofrece. Hay una (o más) biblioteca(s) de Python para (casi) todo:

  • Visualización de datos,

  • Aprendizaje automático,

  • Ciencia de los datos,

  • Procesamiento natural del lenguaje,

  • Análisis de datos complejos.

  • Cálculo científico.

Lo mismo es cierto para los frameworks, que ayudan a despegar su proyecto y le ahorran tiempo y esfuerzo. Hay una variedad de ellos para elegir, dependiendo de sus necesidades, tales como:

  • Django,

  • Flask,

  • Pyramid,

  • Twisted,

  • Falcon.

En lo que respecta a este curso, hablaremos sobre todo de aquellas librerías pertenecientes al stack científico.

Performance

Una de las mayores críticas de Python es el tiempo de ejecución, relativamente lento en comparación con otros lenguajes. Sin embargo, hay una solución alternativa a este desafío específico.

Cuando el rendimiento tiene prioridad, Python le brinda la capacidad de integrar otros lenguajes de mayor rendimiento en su código. Cython es un buen ejemplo de tal solución. Optimiza su velocidad sin obligarlo a reescribir todo su código base desde cero.

El recurso más caro no es el tiempo de CPU, sino el tiempo de tus desarrolladores. Por lo tanto, reducir el tiempo de comercialización siempre debe tener prioridad sobre la ejecución rápida del tiempo de ejecución. Puedes implementar rápidamente en Python y en el caso de necesitar más performance es posible cambiar la implementación a lenguajes compilados como C o Rust.

Python es intuitivo de leer, ya que se parece al inglés real, esto hace que el lenguaje sea fácil de descifrar y mantener. Además tiene una sintaxis clara y no requiere tantas líneas de código como Java o C para obtener resultados comparables.

Zen de Python

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

El Zen de Python te dará la guía para decidir sobre que hacer con tu código, no te dice como lo debes escribir, sino como debes pensar si estas programando en Python.

Principios importantes:

  • Explícito es mejor que implícito: Que no se asuma nada, asegúrate que las cosas sean.

  • Simple es mejor que complejo: Evita código complejo, código espagueti o que hace mas cosas para poder hacer una simple tarea.

  • Plano es mejor que anidado: Si tu código tiene mas de 3 niveles de identación, deberías mover parte de ese código a una función.

  • Los errores nunca deberían pasar silenciosamente: No uses un Try/Except sin definir que tipo de error vas a cachar, viene de la mano con Explicito es mejor que implícito.

  • Si la implementación es difícil de explicar, es mala idea.

Buenas Prácticas

Nombres de variables y comentarios

Asigna nombres a las variables que tengan sentido o que puedan ser identificadas fácilmente, no importa que sean utilices más caracteres. Un mal ejemplo es el siguiente:

a = 10.
b = 3.5
print(f"El área del triángulo es {a * b / 2}")
El área del triángulo es 17.5

¿Si otra persona lee este código puede saber qué significan a y b? Lo puede saber por el comentario, pero sin embargo esas variables podrían ser utilizadas nuevamente y se perdería su significado al pasar las líneas de código. Recuerda, explícito es mejor que implícito.

altura = 10.
base = 3.5
print(f"El área del triángulo es {altura * base / 2}")
El área del triángulo es 17.5

Si con las nombres de las variables no basta, una buena práctica es comentar el código, ya sea antes de varías líneas o comentar una línea en particular.

# Se define la altura y base del triángulo
altura = 10.  # en metros
base = 3.5  # en metros
# Se calcula el área del triángulo
area = altura * base / 2
print(f"El área del triángulo es {area}")
El área del triángulo es 17.5

Documentación

Casi tan importante como la escritura de código, es su correcta documentación, una parte fundamental de cualquier programa que a menudo se infravalora o simplemente se ignora. Aparte de los comentarios entre el código explicando cómo funciona, el elemento básico de documentación de Python es el Docstring o cadena de documentación, que ya hemos visto. Simplemente es una cadena de texto con triple comillas que se coloca justo después de la definición de función o clase que sirve de documentación a ese elemento.

def potencia(x, y):
    """
    Calcula la potencia arbitraria de un numero
    """
    return x ** y
# Acceso a la documentación
print(potencia.__doc__)
    Calcula la potencia arbitraria de un numero
    
# Acceso a la documentación
help(potencia)
Help on function potencia in module __main__:

potencia(x, y)
    Calcula la potencia arbitraria de un numero
potencia?

Lo correcto es detallar lo mejor posible en el Docstring, ya sea funcionamienot, parámetros, valores que retorna, etc. Veamos un ejemplo de la función potencia mejor documentada:

def potencia(x, y):
    """
    power(x1, x2[, out])

    First array elements raised to powers from second array, element-wise.

    Raise each base in `x1` to the positionally-corresponding power in
    `x2`.  `x1` and `x2` must be broadcastable to the same shape. Note that an
    integer type raised to a negative integer power will raise a ValueError.

    Parameters
    ----------
    x1 : array_like
        The bases.
    x2 : array_like
        The exponents.

    Returns
    -------
    y : ndarray
        The bases in `x1` raised to the exponents in `x2`.

    See Also
    --------
    float_power : power function that promotes integers to float
    
    
    Examples
    --------
    Cube each element in a list.

    >>> x1 = range(6)
    >>> x1
    [0, 1, 2, 3, 4, 5]
    >>> np.power(x1, 3)
    array([  0,   1,   8,  27,  64, 125])

    Raise the bases to different exponents.

    >>> x2 = [1.0, 2.0, 3.0, 3.0, 2.0, 1.0]
    >>> np.power(x1, x2)
    array([  0.,   1.,   8.,  27.,  16.,   5.])

    The effect of broadcasting.

    >>> x2 = np.array([[1, 2, 3, 3, 2, 1], [1, 2, 3, 3, 2, 1]])
    >>> x2
    array([[1, 2, 3, 3, 2, 1],
           [1, 2, 3, 3, 2, 1]])
    >>> np.power(x1, x2)
    array([[ 0,  1,  8, 27, 16,  5],
           [ 0,  1,  8, 27, 16,  5]])
       
    """
    return x**y
# Acceso a la documentación
print(potencia.__doc__)
    power(x1, x2[, out])

    First array elements raised to powers from second array, element-wise.

    Raise each base in `x1` to the positionally-corresponding power in
    `x2`.  `x1` and `x2` must be broadcastable to the same shape. Note that an
    integer type raised to a negative integer power will raise a ValueError.

    Parameters
    ----------
    x1 : array_like
        The bases.
    x2 : array_like
        The exponents.

    Returns
    -------
    y : ndarray
        The bases in `x1` raised to the exponents in `x2`.

    See Also
    --------
    float_power : power function that promotes integers to float
    
    
    Examples
    --------
    Cube each element in a list.

    >>> x1 = range(6)
    >>> x1
    [0, 1, 2, 3, 4, 5]
    >>> np.power(x1, 3)
    array([  0,   1,   8,  27,  64, 125])

    Raise the bases to different exponents.

    >>> x2 = [1.0, 2.0, 3.0, 3.0, 2.0, 1.0]
    >>> np.power(x1, x2)
    array([  0.,   1.,   8.,  27.,  16.,   5.])

    The effect of broadcasting.

    >>> x2 = np.array([[1, 2, 3, 3, 2, 1], [1, 2, 3, 3, 2, 1]])
    >>> x2
    array([[1, 2, 3, 3, 2, 1],
           [1, 2, 3, 3, 2, 1]])
    >>> np.power(x1, x2)
    array([[ 0,  1,  8, 27, 16,  5],
           [ 0,  1,  8, 27, 16,  5]])
       
    
# Acceso a la documentación
help(potencia)
Help on function potencia in module __main__:

potencia(x, y)
    power(x1, x2[, out])
    
    First array elements raised to powers from second array, element-wise.
    
    Raise each base in `x1` to the positionally-corresponding power in
    `x2`.  `x1` and `x2` must be broadcastable to the same shape. Note that an
    integer type raised to a negative integer power will raise a ValueError.
    
    Parameters
    ----------
    x1 : array_like
        The bases.
    x2 : array_like
        The exponents.
    
    Returns
    -------
    y : ndarray
        The bases in `x1` raised to the exponents in `x2`.
    
    See Also
    --------
    float_power : power function that promotes integers to float
    
    
    Examples
    --------
    Cube each element in a list.
    
    >>> x1 = range(6)
    >>> x1
    [0, 1, 2, 3, 4, 5]
    >>> np.power(x1, 3)
    array([  0,   1,   8,  27,  64, 125])
    
    Raise the bases to different exponents.
    
    >>> x2 = [1.0, 2.0, 3.0, 3.0, 2.0, 1.0]
    >>> np.power(x1, x2)
    array([  0.,   1.,   8.,  27.,  16.,   5.])
    
    The effect of broadcasting.
    
    >>> x2 = np.array([[1, 2, 3, 3, 2, 1], [1, 2, 3, 3, 2, 1]])
    >>> x2
    array([[1, 2, 3, 3, 2, 1],
           [1, 2, 3, 3, 2, 1]])
    >>> np.power(x1, x2)
    array([[ 0,  1,  8, 27, 16,  5],
           [ 0,  1,  8, 27, 16,  5]])

PEP8

Además de una correcta y ordenada estructura general que deben tener los programa, es conveniente mantener ciertas buenas prácticas de codificación y el estilo de codificación recomendado. Estas normas no son obligatorias, como lo es la propia sintaxis del lenguaje, pero conviene seguir las recomendaciones de los desarrolladores de Python para facilitar la lectura del programa y ayudar a encontrar posibles errores. Incluso dentro de PEP8 existen “sabores” más estrictos, uno que ha ganado fama en el último tiempo es Black.

Manejo de excepciones

Los errores que se producen en tiempo de ejecución se denominan excepciones. Ocurren, por ejemplo, cuando un archivo que intentamos abrir no existe FileNotFoundError, dividiendo un número por cero ZeroDivisionError, etc.

Si no se manejan las excepciones, se escupe un mensaje de error y nuestro programa se detiene repentinamente e inesperadamente.

En Python, las excepciones se pueden manejar usando la declaración try. Cuando se detectan excepciones, depende de ti qué operador realizar, pero siempre puedes capturar todas las exepciones.

Ejemplo: Obtener el inverso multiplicativo de una lista de elementos.

lista_con_errores = [2, 2.3, 0, None, "a"]
for x in lista_con_errores:
    print("Intentando dividir 1 por", x)
    r = 1 / x
    print(f"1 dividido {x} es {r}\n")
Intentando dividir 1 por 2
1 dividido 2 es 0.5

Intentando dividir 1 por 2.3
1 dividido 2.3 es 0.4347826086956522

Intentando dividir 1 por 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-12-43d6566e765c> in <module>
      2 for x in lista_con_errores:
      3     print("Intentando dividir 1 por", x)
----> 4     r = 1 / x
      5     print(f"1 dividido {x} es {r}\n")

ZeroDivisionError: division by zero

Problema! División por cero! La forma de caputar los errores es la siguiente:

import sys

lista_con_errores = [2, 2.3, 0, None, "a"]
for x in lista_con_errores:
    try:
        print("Intentando dividir 1 por", x)
        r = 1 / x
        print(f"1 dividido {x} es {r}\n")
    except:
        exc_type, exc_obj, exc_tb = sys.exc_info()
        print(f"Oops! Ocurrió una excepción del tipo: {exc_type.__name__} y mensaje: {exc_obj}\n")
Intentando dividir 1 por 2
1 dividido 2 es 0.5

Intentando dividir 1 por 2.3
1 dividido 2.3 es 0.4347826086956522

Intentando dividir 1 por 0
Oops! Ocurrió una excepción del tipo: ZeroDivisionError y mensaje: division by zero

Intentando dividir 1 por None
Oops! Ocurrió una excepción del tipo: TypeError y mensaje: unsupported operand type(s) for /: 'int' and 'NoneType'

Intentando dividir 1 por a
Oops! Ocurrió una excepción del tipo: TypeError y mensaje: unsupported operand type(s) for /: 'int' and 'str'

En proyectos más serios, se busca capturar cada tipo de excepción, por ejemplo:

lista_con_errores = [2, 2.3, 0, None, "a"]
for x in lista_con_errores:
    try:
        print("Intentando dividir 1 por", x)
        r = 1 / x
        print(f"1 dividido {x} es {r}\n")
    except ZeroDivisionError as e:
        print(e)
        print("No puedes dividir por cero!\n")
    except TypeError as e:
        print(e)
        print(f"No es posible dividir un 1 por un objeto de tipo {type(x).__name__}!\n")
Intentando dividir 1 por 2
1 dividido 2 es 0.5

Intentando dividir 1 por 2.3
1 dividido 2.3 es 0.4347826086956522

Intentando dividir 1 por 0
division by zero
No puedes dividir por cero!

Intentando dividir 1 por None
unsupported operand type(s) for /: 'int' and 'NoneType'
No es posible dividir un 1 por un objeto de tipo NoneType!

Intentando dividir 1 por a
unsupported operand type(s) for /: 'int' and 'str'
No es posible dividir un 1 por un objeto de tipo str!

Programación Orientada a Objetos

Todo en Python es un objeto que incluye enteros, flotantes, funciones, clases y Ninguno. No nos centremos en por qué todo en Python es un objeto. Para eso, visite esta página. Más bien, esta sección se enfoca en crear sus propias clases y objetos.

El objeto es simplemente una colección de datos (variables) y métodos (funciones) que actúan sobre los datos. Y, la clase es un modelo para el objeto.

Tan pronto como defina una clase, se crea un nuevo objeto de clase con el mismo nombre. Este objeto de clase nos permite acceder a los diferentes atributos, así como crear instancias de nuevos objetos de esa clase.

class Mi_clase:
    "Esta es mi clase"
    a = 10
    def func(self):
        print('hola')

# Output: 10
print(Mi_clase.a)

# Output: <function 0x0000000003079bf8="" at="" myclass.func="">
print(Mi_clase.func)

# Output: 'Esta es mi clase'
print(Mi_clase.__doc__)
10
<function Mi_clase.func at 0x7f4cb04e6320>
Esta es mi clase

Es posible que haya notado el parámetro self en la definición de la función dentro de la clase, pero llamamos al método simplemente como ob.func() sin ningún argumento. Aún funcionó.

Esto se debe a que, cada vez que un objeto llama a su método, el objeto mismo se pasa como primer argumento. Entonces, ob.func() se traduce en Mi_clase.func(ob).

Instanciar objetos también es muy sencillo.

obj1 = Mi_clase()
print(obj1.a)        # Output: 10
 
obj2 = Mi_clase()
print(obj1.a + 5)    # Output: 15
10
15

En Python, un método con el nombre __init () __ es un constructor. Este método se llama automáticamente cuando se instancia un objeto.

class NumerosComplejos:
    
    def __init__(self, r=0, i=0):  # constructor
        self.real = r
        self.imag = i

    def obtener_representacion(self):
        print("{0} + {1}j".format(self.real, self.imag))
c1 = NumerosComplejos(2,3) # crear el objeto NumerosComplejos
c1.obtener_representacion() # Output: 2+3j
2 + 3j
c2 = NumerosComplejos() # crear un nuevo objeto NumerosComplejos
c2.obtener_representacion() # Output: 0+0j
0 + 0j

La herencia se refiere a definir una nueva clase con poca o ninguna modificación a una clase existente. Tomemos un ejemplo:

class Mamifero:

    def __init__(self, nombre="Sin Nombre"):
        self.nombre = nombre

    def caracteristicas(self):
        print ('Los mamíferos son animales de sangre caliente')

Derivemos una nueva clase Perro de esta clase Mamifero.

class Perro(Mamifero):
    
    numero_patas = 4
    
    def ladrar(self):
        print(f'{self.nombre}: Guau!')

firulais = Perro("Firulais")
firulais.ladrar()
print(f"{firulais.nombre} tiene {firulais.numero_patas} patas.")
firulais.caracteristicas()
Firulais: Guau!
Firulais tiene 4 patas.
Los mamíferos son animales de sangre caliente

Lo importante aquí es que notes que hay elementos que pertenecen a cada perro, como su nombre y la cantidad de patas. Pero también hay acciones que puede realizar un perro. En programación orientada a objetos, esto se conoce como atributos y métodos respectivamente.

Decoradores

Python tiene una característica interesante llamada decoradores para agregar funcionalidad a un código existente. Esto también se llama metaprogramación ya que una parte del programa intenta modificar otra parte del programa en tiempo de compilación.

def debug(f):
    def nueva_funcion(a, b):
        print("La funcion Sumar es llamada!!!")
        return f(a, b)
    return nueva_funcion


@debug # decorador
def Sumar(a, b):
    return a + b
print(Sumar(7, 5))
La funcion Sumar es llamada!!!
12

Hay decoradores especiales que se utilizan al declarar clases, por ejemplo classmethod.

import random, string

class Gato(Mamifero):

    numero_patas = 4

    def maullar(self):
        print(f'{self.nombre}: Miau!')
    
    @classmethod
    def random_name(cls, length=8):
        letras = string.ascii_lowercase
        nombre = ''.join(random.choice(letras) for i in range(length))
        return cls(nombre=nombre)
gañaña = Gato(nombre="Gañaña")
gañaña.maullar()
Gañaña: Miau!
random_gato = Gato.random_name(length=10)
random_gato.maullar()
kjfebccwti: Miau!

No te preocupes si parece chino mandarín, en el curso no crearemos nuestras propias clases, si no que aprenderemos a leer documentación para utilizar las clases que ya han sido implementadas.