Nuevas características incorporadas en Python 3.7

Nuevas caracteristicas en Python 3.7

Nuevas características incorporadas en Python 3.7

IMPORTANTE: Este post acerca de Python 3.7 es solo una traducción del increíble trabajo realizado por el equipo de RealPython en su publicación (ver publicación original). El permiso de llevar a cabo esta publicación lo encuentran aquí, me pareció grandiosa la explicación y quería llevarlo a conocer a más gente, todo el crédito es para ellos 🙂

¡Python 3.7 se lanzó oficialmente ! Esta nueva versión de Python ha estado en desarrollo desde septiembre de 2016 , y ahora todos disfrutamos de los resultados del arduo trabajo por parte de los desarrolladores de este grandioso lenguaje.

¿Qué trae la nueva versión de Python? Si bien la documentación ofrece una visión general de las nuevas funciones, este artículo profundizará en algunas de las noticias más importantes. Éstas incluyen:

  • Acceso más fácil a los depuradores a través de un nuevo método incorporado breakpoint()
  • Simple creación de clases usando clases de datos
  • Acceso personalizado a los atributos del módulo
  • Soporte mejorado para sugerencias tipo
  • Funciones de temporización de mayor precisión

Más importante aún, Python 3.7 es rápido.

En las secciones finales de este artículo, leerá más sobre esta velocidad, así como algunas de las otras características geniales de Python 3.7. También recibirá algunos consejos sobre la actualización a la nueva versión.

El método breakpoint()

Si bien podemos esforzarnos por escribir un código perfecto, la verdad es que nunca lo hacemos. La depuración es una parte importante de la programación. Python 3.7 presenta la nueva función incorporada breakpoint(). Esto realmente no agrega ninguna funcionalidad nueva a Python, pero hace que usar depuradores sea más flexible e intuitivo.

Supongamos que tenemos el siguiente código buggy en el archivo bugs.py:

def divide(e, f):
    return f / e

a, b = 0, 1
print(divide(a, b))

Ejecutar el código causa un ZeroDivisionError dentro de la función divide() Digamos que queremos interrumpir su código y colocarlo en un depurador en la parte superior de divide(). Podemos hacerlo configurando un  “punto de interrupción” en el código:

def divide(e, f):
    # Insert breakpoint here
    return f / e

Un punto de interrupción es una señal dentro de nuestro código, de que la ejecución debería detenerse temporalmente para que pueda observar el estado actual del programa. ¿Cómo colocamos el punto de interrupción? En Python 3.6 y anteriores,  se utiliza algo como lo siguiente:

def divide(e, f):
    import pdb; pdb.set_trace()
    return f / e

Aquí, pdb, es el depurador de Python incluido en la biblioteca estándar. En Python 3.7, podemos utilizar el método breakpoint()como un acceso directo en su lugar:

def divide(e, f):
    breakpoint()
    return f / e

Tras bambalinas, breakpoint()primero está importando a pdby luego llamando pdb.set_trace()por nosotros. Los beneficios obvios son que breakpoint()es más fácil de recordar y que solo necesitamos escribir 12 caracteres en lugar de 27. Sin embargo, la verdadera ventaja del uso de breakpoint()es su personalización.

Ejecutamos bugs.py con breakpoint():

$ python3.7 bugs.py 
> /home/gahjelle/bugs.py(3)divide()
-> return f / e
(Pdb)

El script se romperá cuando breakpoint()sea ejecutado y nos llevará a una sesión de depuración de PDB. En este punto podemos escribir c y presionar Enter para continuar la secuencia de comandos. Consulte la guía PDB de Nathan Jennings si deseas obtener más información sobre PDB y la depuración.

Ahora, digamos que hemos arreglado el error. Nos gustaría ejecutar el script nuevamente pero sin detenerse en el depurador. Podríamos, por supuesto, comentar la línea donde invocamos breakpoint(), pero otra opción es usar la variable de entorno PYTHONBREAKPOINT. Esta variable controla el comportamiento de breakpoint(), y la configuración PYTHONBREAKPOINT=0 significa que cualquier llamada a breakpoint()es ignorada:

$ PYTHONBREAKPOINT=0 python3.7 bugs.py
ZeroDivisionError: division by zero

Genial, parece que no hemos solucionado el error después de todo…

Otra opción es usar PYTHONBREAKPOINT para especificar un depurador que no sea PDB. Por ejemplo, para usar PuDB (un depurador visual en la consola) podemos hacer:

$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py

Para que esto funcione, como podrás imaginar, debemos tener pudb instalado (pip install pudb). Python se encargará de importar pudbpor nosotros. De esta forma también se puede configurar un depurador predeterminado. Simplemente configuramos la variable de entorno PYTHONBREAKPOINT con nuestro depurador preferido. Consulta esta guía para obtener instrucciones sobre cómo configurar una variable de entorno en tu sistema.

La nueva función breakpoint() no solo funciona con depuradores. Una opción conveniente podría ser simplemente iniciar un shell interactiva dentro de nuestro código. Por ejemplo, para iniciar una sesión de IPython, podemos usar lo siguiente:

$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py 
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print(e / f)
0.0

También podemos crear nuestra propia función e invocar breakpoint(). El siguiente código imprime todas las variables en el alcance local. Añádelo a un archivo llamado bp_utils.py:


from pprint import pprint
import sys

def print_locals():
    caller = sys._getframe(1)  # Caller is 1 frame up.
    pprint(caller.f_locals)

Para usar esta función, necesitamos configurar PYTHONBREAKPOINT como antes, con la notación <module>.<function>:

$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py 
{'e': 0, 'f': 1}
ZeroDivisionError: division by zero

Normalmente, breakpoint()se usará para llamar a funciones y métodos que no necesitan argumentos. Sin embargo, es posible pasar argumentos también. Únicamente necesitamos cambiar la línea breakpoint()en bugs.pya:

breakpoint(e, f, end="<-END\n")

Nota: El depurador predeterminado (PDB) generará un TypeError en esta línea porque pdb.set_trace()no toma ningún argumento posicional.

Ejecutemos este código con breakpoint() enmascarando a la función print() para ver un ejemplo simple de los argumentos que se pasan:

$ PYTHONBREAKPOINT=print python3.7 bugs.py 
0 1<-END
ZeroDivisionError: division by zero

Puedes ver PEP 553 , así como la documentación de breakpoint()sys.breakpointhook()para obtener más información.

Clases de Datos (Data Classes)

El nuevo módulo dataclasses hace que sea más conveniente de escribir nuestras propias clases, así como los métodos especiales .__init__().__repr__().__eq__()se añaden automáticamente. Usando el decorador @dataclass, podemos escribir algo como:

from dataclasses import dataclass, field

@dataclass(order=True)
class Country:
    name: str
    population: int
    area: float = field(repr=False, compare=False)
    coastline: float = 0

    def beach_per_person(self):
        """Meters of coastline per person"""
        return (self.coastline * 1000) / self.population

Estas nueve líneas de código representan un buen código repetitivo y las mejores prácticas. Piense en lo que llevaría implementar Countrycomo una clase regular: el .__init__()método, una repr, seis diferentes métodos de comparación, así como el método .beach_per_person().  A continuación podremos ver una implementación de Country que es más o menos equivalente a la dataclass:


class Country:

    def __init__(self, name, population, area, coastline=0):
        self.name = name
        self.population = population
        self.area = area
        self.coastline = coastline

    def __repr__(self):
        return (
            f"Country(name={self.name!r}, population={self.population!r},"
            f" coastline={self.coastline!r})"
        )

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                == (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __ne__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                != (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __lt__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) &amp;amp;amp;amp;amp;amp;amp;lt; (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __le__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) &amp;amp;amp;amp;amp;amp;amp;lt;= ( other.name, other.population, other.coastline )) return NotImplemented def __gt__(self, other): if other.__class__ is self.__class__: return ((self.name, self.population, self.coastline) &amp;amp;amp;amp;amp;amp;amp;gt; (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __ge__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) &amp;amp;amp;amp;amp;amp;amp;gt;= (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def beach_per_person(self):
        """Meters of coastline per person"""
        return (self.coastline * 1000) / self.population

Después de la creación, una clase de datos es una clase normal. Podemos, por ejemplo, heredar de una clase de datos de la manera normal. El objetivo principal de las dataclass es hacer que sea rápido y fácil escribir clases sólidas, en particular clases pequeñas que almacenan principalmente datos.

Puede usar la dataclass Country como cualquier otra clase:

>>> norway = Country("Norway", 5320045, 323802, 58133)
>>> norway
Country(name='Norway', population=5320045, coastline=58133)

>>> norway.area
323802

>>> usa = Country("United States", 326625791, 9833517, 19924)
>>> nepal = Country("Nepal", 29384297, 147181)
>>> nepal
Country(name='Nepal', population=29384297, coastline=0)

>>> usa.beach_per_person()
0.06099946957342386

>>> norway.beach_per_person()
10.927163210085629

Tengamos en cuenta que todos los campos .name.population.area, y .coastlinese utilizan cuando se realiza la inicialización de la clase (aunque .coastlinees opcional, como se muestra en el ejemplo de litoral Nepal). La clase Country tiene un reprmétodo razonable , mientras que la definición funciona igual que para las clases regulares.

Por defecto, las dataclasses se pueden comparar por igualdad. Como especificamos order=True en el decorador @dataclass, la clase Country también se puede ordenar:


>>> norway == norway
True

>>> nepal == usa
False

>>> sorted((norway, usa, nepal))
[Country(name='Nepal', population=29384297, coastline=0),
 Country(name='Norway', population=5320045, coastline=58133),
 Country(name='United States', population=326625791, coastline=19924)]

La clasificación ocurre en los valores de cada campo, primero .name luego .population, y así sucesivamente. Sin embargo, si usamos field(), podemos personalizar qué campos se usarán en la comparación. En el ejemplo, el campo .area se dejó fuera de repry las comparaciones.

Nota: Los datos del país provienen del CIA World Factbook con cifras de población estimadas para julio de 2017.

Antes de que todos vayan a reservar sus próximas vacaciones en la playa en Noruega, esto es lo que dice Factbook sobre el clima noruego : “templado a lo largo de la costa, modificado por la corriente del Atlántico Norte; un interior más frío con mayor precipitación y veranos más fríos; lluvioso todo el año en la costa oeste “.

Las dataclasses hacen algunas de las mismas cosas que las namedtuple. Sin embargo, obtienen su mayor inspiración del proyecto attrs . Consulte nuestra guía completa de clases de datos para obtener más ejemplos e información adicional, así como PEP 557 para la descripción oficial.

Personalización de los atributos del módulo

¡Los atributos están en todas partes en Python! Si bien los atributos de clase son probablemente los más famosos, los atributos se pueden poner esencialmente en cualquier cosa, incluidas las funciones y los módulos. Varias de las características básicas de Python se implementan como atributos: la mayoría de la funcionalidad de introspección, doc-strings y espacios de nombres. Las funciones dentro de un módulo están disponibles como atributos de módulo.

Los atributos más a menudo se recuperan con la notación de puntos: thing.attribute. Sin embargo, también puede obtener los atributos que se nombran en tiempo de ejecución utilizando getattr():


import random

random_attr = random.choice(("gammavariate", "lognormvariate", "normalvariate"))
random_func = getattr(random, random_attr)

print(f"A {random_attr} random value: {random_func(1, 1)}")

Ejecutar este código producirá algo como:

A gammavariate random value: 2.8017715125270618

Para las clases, las llamadas thing.attrprimero buscarán attrdefinidas en thing. Si no se encuentra, entonces se llama al método especial thing.__getattr__("attr"). (Esto es una simplificación. Consulte este artículo para obtener más detalles.) El método .__getattr__() se puede usar para personalizar el acceso a los atributos en los objetos.

Hasta Python 3.7, la misma personalización no estaba disponible fácilmente para los atributos del módulo. Sin embargo, PEP 562 presenta __getattr__()en los módulos, junto con una función __dir__() correspondiente . La función especial __dir__() permite la personalización del resultado de llamar dir()hacia un módulo.

El PEP en sí proporciona algunos ejemplos de cómo se pueden usar estas funciones, incluyendo la adición de advertencias de desuso a las funciones y la carga diferida de los submódulos pesados. A continuación, crearemos un sistema de complemento simple que permite que las funciones se agreguen dinámicamente a un módulo. Este ejemplo aprovecha los paquetes de Python. Consulte este artículo si necesita una actualización sobre los paquetes.

Crearemos un nuevo directorio, pluginsy agregaremos el siguiente código a un archivo plugins/__init__.py:


from importlib import import_module
from importlib import resources

PLUGINS = dict()

def register_plugin(func):
    """Decorator to register plug-ins"""
    name = func.__name__
    PLUGINS[name] = func
    return func

def __getattr__(name):
    """Return a named plugin"""
    try:
        return PLUGINS[name]
    except KeyError:
        _import_plugins()
        if name in PLUGINS:
            return PLUGINS[name]
        else:
            raise AttributeError(
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

def __dir__():
    """List available plug-ins"""
    _import_plugins()
    return list(PLUGINS.keys())

def _import_plugins():
    """Import all resources to register plug-ins"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

Antes de ver qué hace este código, agreguemos dos archivos más dentro del directorio plugins. Primero, veamos plugins/plugin_1.py:

from . import register_plugin

@register_plugin
def hello_1():
    print("Hello from Plugin 1")

A continuación, agreguemos un código similar en el archivo plugins/plugin_2.py:


from . import register_plugin

@register_plugin
def hello_2():
    print("Hello from Plugin 2")

@register_plugin
def goodbye():
    print("Plugin 2 says goodbye")

Estos plugins ahora se pueden usar de la siguiente manera:


>>> import plugins
>>> plugins.hello_1()
Hello from Plugin 1

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.goodbye()
Plugin 2 says goodbye

Esto puede no parecer tan revolucionario (y probablemente no lo sea), pero veamos lo que sucedió aquí. Normalmente, para poder llamar plugins.hello_1(), la función hello_1() debe definirse en un módulo plugins o importarse explícitamente dentro de __init__.py un paquete plugins. ¡Aquí, no es ninguno!

En cambio, hello_1()se define en un archivo arbitrario dentro del paquete plugins y hello_1() se convierte en una parte del paquete plugins registrándose con el decorador @register_plugin.

La diferencia es sutil. En lugar de que el paquete dicte qué funciones están disponibles, las funciones individuales se registran a sí mismas como parte del paquete. Esto le proporciona una estructura simple donde puede agregar funciones independientemente del resto del código sin tener que mantener una lista centralizada de las funciones disponibles.

Hagamos una revisión rápida de lo que hace __getattr__() dentro del código plugins/__init__.py. Cuando se solicitó plugins.hello_1(), Python primero busca una función hello_1() dentro del archivo plugins/__init__.py. Como no existe tal función, Python llama en su lugar a __getattr__("hello_1"). Recuerde el código fuente de la función __getattr__():


def __getattr__(name):
    """Return a named plugin"""
    try:
        return PLUGINS[name]        # 1) Try to return plugin
    except KeyError:
        _import_plugins()           # 2) Import all plugins
        if name in PLUGINS:
            return PLUGINS[name]    # 3) Try to return plugin again
        else:
            raise AttributeError(   # 4) Raise error
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

__getattr__()contiene los siguientes pasos. Los números en la siguiente lista corresponden a los comentarios enumerados en el código:

  1. En primer lugar, la función intenta devolver de manera optimista el plugin nombrado del diccionario PLUGINS. Esto tendrá éxito si existe name un plugin llamado y ya ha sido importado.
  2. Si el plugin indicado no se encuentra en el diccionario PLUGINS, nos aseguramos de que se importen todos los plugins.
  3. Devuelve el plugin indicado si está disponible después de la importación.
  4. Si el plugin no está en el diccionario PLUGINS después de importar todos los plugins, levantamos un AttributeError diciendo que name no es un atributo (plugin) en el módulo actual.

¿Cómo se llena el diccionario PLUGINS? La función _import_plugins() importa todos los archivos Python dentro del paquete plugins, pero no parece tocar PLUGINS:


def _import_plugins():
    """Import all resources to register plug-ins"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

No olvide que cada función de plugin está decorada por el decorador @register_plugin. Se llama a este decorador cuando se importan los plugins y es el que realmente pobla el diccionario PLUGINS. Puede ver esto si importa manualmente uno de los archivos de plugin:


>>> import plugins
>>> plugins.PLUGINS
{}

>>> import plugins.plugin_1
>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>}

Continuando con el ejemplo, tengamos en cuenta que llamar dir()al módulo también importa los plugins restantes:


>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>,
 'hello_2': <function hello_2 at 0x7f29d4341620>,
 'goodbye': <function goodbye at 0x7f29d43416a8>}

dir()generalmente enumera todos los atributos disponibles en un objeto. Normalmente, usar dir()en un módulo da como resultado algo como esto:


>>> import plugins
>>> dir(plugins)
['PLUGINS', '__builtins__', '__cached__', '__doc__',
 '__file__', '__getattr__', '__loader__', '__name__',
 '__package__', '__path__', '__spec__', '_import_plugins',
 'import_module', 'register_plugin', 'resources']

Si bien esto podría ser información útil, estamos más interesados ​​en exponer los plugins disponibles. En Python 3.7,  se puede personalizar el resultado de llamar dir()a un módulo agregando una función especial __dir__(). Para plugins/__init__.py, esta función primero se asegura de que todos los plugins se hayan importado y luego enumera sus nombres:


def __dir__():
    """List available plug-ins"""
    _import_plugins()
    return list(PLUGINS.keys())

Antes de dejar este ejemplo, ten en cuenta que también usamos otra característica nueva y genial de Python 3.7. Para importar todos los módulos dentro del directorio plugins, usamos el nuevo módulo importlib.resources. Este módulo da acceso a archivos y recursos dentro de módulos y paquetes sin la necesidad de __file__hacks (que no siempre funcionan) o pkg_resources(que es lento). Otras características de importlib.resourcesserán resaltadas más adelante .

Mejoras de escritura

Las sugerencias y anotaciones de tipo han estado en constante desarrollo a lo largo de la serie de lanzamientos de Python 3. El sistema de tipeo de Python ahora es bastante estable. Aún así, Python 3.7 trae algunas mejoras a la mesa: mejor rendimiento, soporte central y referencias avanzadas.

Python no realiza ningún tipo de comprobación en el tiempo de ejecución (a menos que esté utilizando paquetes explícitamente como enforce). Por lo tanto, agregar sugerencias de tipo a su código no debería afectar su rendimiento.

Desafortunadamente, esto no es completamente cierto ya que la mayoría de las sugerencias de tipo necesitan el módulo typing. El módulo typing es uno de los módulos más lentos en la biblioteca estándar. PEP 560 agrega soporte básico para tipear en Python 3.7, lo que acelera significativamente el módulo typing. Los detalles de esto en general no son necesarios de saber. Simplemente reclínese y disfrute del mayor rendimiento.

Si bien el sistema de tipos de Python es razonablemente expresivo, un problema que causa cierto dolor es la referencia directa. Las sugerencias de tipo, o más generalmente las anotaciones, se evalúan mientras se importa el módulo. Por lo tanto, todos los nombres ya deben estar definidos antes de ser utilizados. Lo siguiente no es posible:


class Tree:
    def __init__(self, left: Tree, right: Tree) -&amp;amp;amp;amp;gt; None:
        self.left = left
        self.right = right

La ejecución del código genera una NameErrorporque la clase Tree aún no está (completamente) definida en la definición del método .__init__():


Traceback (most recent call last):
  File "tree.py", line 1, in &amp;amp;amp;amp;lt;module&amp;amp;amp;amp;gt;
    class Tree:
  File "tree.py", line 2, in Tree
    def __init__(self, left: Tree, right: Tree) -&amp;amp;amp;amp;gt; None:
NameError: name 'Tree' is not defined

Para superar esto, habría necesitado escribir "Tree"como un literal de cadena en su lugar:


class Tree:
    def __init__(self, left: "Tree", right: "Tree") -&amp;amp;amp;amp;gt; None:
        self.left = left
        self.right = right

Ver PEP 484 para la discusión original.

En un futuro Python 4.0 , se permitirán estas llamadas referencias hacia adelante. Esto se manejará al no evaluar las anotaciones hasta que se solicite explícitamente. PEP 563 describe los detalles de esta propuesta. En Python 3.7, las referencias avanzadas ya están disponibles como __future__import . Ahora puede escribir lo siguiente:


from __future__ import annotations

class Tree:
    def __init__(self, left: Tree, right: Tree) -&amp;amp;amp;amp;gt; None:
        self.left = left
        self.right = right

Ten en cuenta que, además de evitar la sintaxis algo torpe "Tree" ,  la evaluación de anotaciones pospuesta también acelerará su código, ya que las sugerencias de tipo no se ejecutan. Las referencias a futuro ya son compatibles con mypy.

De lejos, el uso más común de las anotaciones es el tipo de insinuación. Aún así, tiene acceso completo a las anotaciones en tiempo de ejecución y puede usarlas como mejor le parezca. Si está manejando anotaciones directamente, debe tratar las posibles referencias directas explícitamente.

Ahora a crear algunos ejemplos ciertamente tontos que muestran cuándo se evalúan las anotaciones. Primero lo hacemos al estilo antiguo, por lo que las anotaciones se evalúan en el momento de la importación. Dejemos que anno.py contenga el siguiente código:


def greet(name: print("Now!")):
    print(f"Hello {name}")

Tenga en cuenta que la anotación de name es print(). Esto es solo para ver exactamente cuándo se evalúa la anotación. Importamos el nuevo módulo:


>>> import anno
Now!

>>> anno.greet.__annotations__
{'name': None}

>>> anno.greet("Alice")
Hello Alice

Como podemos ver, la anotación se evaluó en el momento de la importación. Ten en cuenta que name termina anotado con Noneporque es el valor de retorno de print().

Agrega __future__import para habilitar la evaluación de anotaciones pospuestas:


from __future__ import annotations

def greet(name: print("Now!")):
    print(f"Hello {name}")

La importación de este código actualizado no evaluará la anotación:


>>> import anno

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

>>> anno.greet("Marty")
Hello Marty

Tenga en cuenta que Now!nunca se imprime y la anotación se mantiene como una cadena literal en el diccionario __annotations__. Para evaluar la anotación, use typing.get_type_hints()eval():


>>> import typing
>>> typing.get_type_hints(anno.greet)
Now!
{'name': <class 'NoneType'>}

>>> eval(anno.greet.__annotations__["name"])
Now!

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

Observe que el diccionario __annotations__ nunca se actualiza, por lo que debe evaluar la anotación cada vez que la use.

Precisión de tiempo

En Python 3.7, el módulo time gana algunas funciones nuevas como se describe en PEP 564 . En particular, se agregan las siguientes seis funciones:

  • clock_gettime_ns(): Devuelve el tiempo de un reloj especificado
  • clock_settime_ns(): Establece el tiempo de un reloj especificado
  • monotonic_ns(): Devuelve el tiempo de un reloj relativo que no puede retroceder (por ejemplo, debido a los ahorros de luz diurna)
  • perf_counter_ns(): Devuelve el valor de un contador de rendimiento, un reloj diseñado específicamente para medir intervalos cortos
  • process_time_ns(): Devuelve la suma del tiempo de CPU del sistema y del usuario del proceso actual (sin incluir el tiempo de inactividad)
  • time_ns(): Devuelve el número de nanosegundos desde el 1 de enero de 1970

En cierto sentido, no se agrega ninguna funcionalidad nueva. Cada función es similar a una función ya existente sin el _nssufijo. La diferencia es que las nuevas funciones devuelven una cantidad de nanosegundos como int en lugar de una cantidad de segundos como un float.

Para la mayoría de las aplicaciones, la diferencia entre estas nuevas funciones de nanosegundos y su contraparte anterior no será apreciable. Sin embargo, las nuevas funciones son más fáciles de razonar porque dependen int en vez de float. Los números de punto flotantes son por naturaleza inexactos :


>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Esto no es un problema con Python, sino una consecuencia de las computadoras que necesitan representar números decimales infinitos utilizando un número finito de bits.

Un float en Python sigue el estándar IEEE 754 y usa 53 bits significativos. El resultado es que cualquier tiempo superior a 104 días (2⁵³ o aproximadamente 9 cuatrillones de nanosegundos ) no se puede expresar como un flotador con precisión de nanosegundos. Por el contrario, un intes ilimitado , por lo que un número entero de nanosegundos siempre tendrá una precisión de nanosegundos independiente del valor del tiempo.

Como ejemplo, time.time()devuelve el número de segundos desde el 1 de enero de 1970. Este número ya es bastante grande, por lo que la precisión de este número se encuentra en el nivel de microsegundos. Esta función es la que muestra la mayor mejora en su _nsversión. La resolución de time.time_ns()es aproximadamente 3 veces mejor que para time.time().

Por otro lado, si necesita trabajar con fechas con precisión de nanosegundos, datetime de la biblioteca estándar no lo cortará. Explícitamente solo maneja microsegundos:


>>> from datetime import datetime, timedelta
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-6)
datetime.datetime(2018, 6, 27, 0, 0, 0, 1)

>>> datetime(2018, 6, 27) + timedelta(seconds=1e-9)
datetime.datetime(2018, 6, 27, 0, 0)

En cambio, puedes usar el proyecto astropy. Su paquete astropy.time representa las fechas usando dos objetos float que garantizan “una precisión inferior a nanosegundos en tiempos que abarcan la edad del universo”.


>>> from astropy.time import Time, TimeDelta
>>> Time("2018-06-27")
<Time object: scale='utc' format='iso' value=2018-06-27 00:00:00.000>

>>> t = Time("2018-06-27") + TimeDelta(1e-9, format="sec")
>>> (t - Time("2018-06-27")).sec
9.976020010071807e-10

La última versión de astropy está disponible en Python 3.5 y versiones posteriores.

Otras características bastante geniales

Hasta ahora, ha visto las noticias de los titulares sobre las novedades de Python 3.7. Sin embargo, hay muchos otros cambios que también son geniales. En esta sección, veremos brevemente algunos de ellos.

El orden de los diccionarios está garantizado

La implementación de CPython de Python 3.6 ha ordenado diccionarios. ( PyPy también tiene esto.) Esto significa que los elementos en los diccionarios se repiten en el mismo orden en que se insertaron. El primer ejemplo es usando Python 3.5 y el segundo es usando Python 3.6:


>>> {"one": 1, "two": 2, "three": 3}  # Python <= 3.5 {'three': 3, 'one': 1, 'two': 2} >>> {"one": 1, "two": 2, "three": 3}  # Python >= 3.6
{'one': 1, 'two': 2, 'three': 3}

En Python 3.6, este orden fue solo una buena consecuencia de esa implementación de dict. En Python 3.7, sin embargo, los diccionarios que conservan su orden de inserción son parte de la especificación del lenguaje . Como tal, ahora se puede confiar en proyectos que solo admiten Python>= 3.7 (o CPython>= 3.6).

async” Y “await” son palabras clave

Python 3.5 introdujo corutinas con sintaxis asyncawait . Para evitar problemas de compatibilidad con versiones anteriores, asyncawaitno se agregaron a la lista de palabras clave reservadas. En otras palabras, aún era posible definir variables o funciones nombradas asyncawait.

En Python 3.7, esto ya no es posible:


>>> async = 1
  File "<stdin>", line 1
    async = 1
          ^
SyntaxError: invalid syntax

>>> def await():
  File "<stdin>", line 1
    def await():
            ^
SyntaxError: invalid syntax

” asyncio” Lifting facial

asyncio parte de la biblioteca estándar se introdujo originalmente en Python 3.4 para manejar la concurrencia de una manera moderna mediante bucles de eventos, corrutinas y futuros. Aquí hay una introducción gentil .

En Python 3.7, el módulo asyncio obtiene un importante lavado de cara , que incluye muchas funciones nuevas, soporte para las variables de contexto (ver a continuación ) y mejoras de rendimiento. De particular interés es asyncio.run(), que simplifica la llamada de corrutinas desde código síncrono. Usando asyncio.run(), no es necesario crear explícitamente el ciclo de eventos. Ahora se puede escribir un programa Hello World asíncrono:


import asyncio

async def hello_world():
    print("Hello World!")

asyncio.run(hello_world())

Variables de contexto

Las variables de contexto son variables que pueden tener valores diferentes según su contexto. Son similares al almacenamiento Thread-Local en el que cada subproceso de ejecución puede tener un valor diferente para una variable. Sin embargo, con las variables de contexto, puede haber varios contextos en un hilo de ejecución. El caso de uso principal para las variables de contexto es el seguimiento de las variables en las tareas asincrónicas concurrentes.

El siguiente ejemplo construye tres contextos, cada uno con su propio valor para el valor name. La  funcióngreet() luego puede usar el valor de name dentro de cada contexto:


import contextvars

name = contextvars.ContextVar("name")
contexts = list()

def greet():
    print(f"Hello {name.get()}")

# Construct contexts and set the context variable name
for first_name in ["Steve", "Dina", "Harry"]:
    ctx = contextvars.copy_context()
    ctx.run(name.set, first_name)
    contexts.append(ctx)

# Run greet function inside each context
for ctx in reversed(contexts):
    ctx.run(greet)

La ejecución de este script saluda a Steve, Dina y Harry en orden inverso:


$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve

Importar archivos de datos con ” importlib.resources

Un desafío al empaquetar un proyecto de Python es decidir qué hacer con los recursos del proyecto, como los archivos de datos que necesita el proyecto. Algunas opciones se han usado comúnmente:

  • Hard-code una ruta al archivo de datos.
  • Coloque el archivo de datos dentro del paquete y ubíquelo usando __file__.
  • Use setuptools.pkg_resourcespara acceder al recurso de archivo de datos.

Cada uno de estos tiene sus defectos. La primera opción no es portátil. El uso __file__es más portátil, pero si el proyecto Python está instalado, podría terminar dentro de un archivo comprimido y no tener un __file__atributo. La tercera opción resuelve este problema, pero desafortunadamente es muy lenta.

Una mejor solución es el nuevo módulo importlib.resources en la biblioteca estándar. Utiliza la funcionalidad de importación existente de Python para importar también archivos de datos. Supongamos que tiene un recurso dentro de un paquete de Python como este:

data/
│
├── alice_in_wonderland.txt
└── __init__.py

Ten en cuenta que data debe ser un paquete de Python. Es decir, el directorio debe contener un archivo __init__.py (que puede estar vacío). A continuación, puedes leer el archivoalice_in_wonderland.txtde la siguiente manera:


>>> from importlib import resources
>>> with resources.open_text("data", "alice_in_wonderland.txt") as fid:
...     alice = fid.readlines()
... 
>>> print("".join(alice[:7]))
CHAPTER I. Down the Rabbit-Hole

Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, ‘and what is the use of a book,’ thought Alice ‘without pictures or
conversations?’

Una función similar resources.open_binary() está disponible para abrir archivos en modo binario. En el ejemplo anterior de “complementos como atributos del módulo” , solíamos  usarimportlib.resources para descubrir los plugins disponibles usando resources.contents(). Vea la charla PyCon 2018 de Barry Warsaw para más información.

Se puede usar importlib.resourcesen Python 2.7 y Python 3.4+ a través de un backport . Una guía sobre la migración de pkg_resourcesqueimportlib.resources está disponible.

Trucos del desarrollador

Python 3.7 ha agregado varias características dirigidas a usted como desarrollador. Ya has visto el nuevo breakpoint()built-in . Además, se han agregado algunas nuevas -Xopciones de línea de comando al intérprete de Python.

Puede hacerse una idea de cuánto tiempo tardan las importaciones en su script, usando -X importtime:


$ python3.7 -X importtime my_script.py
import time: self [us] | cumulative | imported package
import time:      2607 |       2607 | _frozen_importlib_external
...
import time:       844 |      28866 |   importlib.resources
import time:       404 |      30434 | plugins

La  columnacumulative muestra el tiempo acumulado de importación (en microsegundos). En este ejemplo, la importación pluginsduró unos 0,03 segundos, la mayor parte de lo cual se gastó importando importlib.resources. La columna self muestra el tiempo de importación excluyendo las importaciones anidadas.

Ahora puede usar -X devpara activar el “modo de desarrollo”. El modo de desarrollo agregará ciertas características de depuración y comprobaciones de tiempo de ejecución que se consideran demasiado lentas para habilitarse de manera predeterminada. Estos incluyen la habilitación faulthandlerde mostrar un rastreo en bloqueos serios, así como más advertencias y enlaces de depuración.

Finalmente, -X utf8habilita el modo UTF-8 . (Consulte PEP 540 ). En este modo, UTF-8se usará para la codificación de texto independientemente de la configuración regional actual.

Optimizaciones

Cada nueva versión de Python viene con un conjunto de optimizaciones. En Python 3.7, hay algunas aceleraciones significativas, que incluyen:

  • Hay menos gastos indirectos al llamar a muchos métodos en la biblioteca estándar.
  • Las llamadas a métodos son hasta un 20% más rápidas en general.
  • El tiempo de inicio de Python se reduce en un 10-30%.
  • La importación typinges 7 veces más rápida.

Además, se incluyen muchas más optimizaciones especializadas. Vea esta lista para una descripción detallada.

El resultado de todas estas optimizaciones es que Python 3.7 es rápido . Es simplemente la versión más rápida de CPython lanzada hasta ahora.

Entonces, ¿Debería actualizar?

Comencemos con la respuesta simple. Si quiere probar cualquiera de las nuevas características que ha visto aquí, entonces necesita poder usar Python 3.7. El uso de herramientas como Anacondapyenv hace que sea fácil tener varias versiones de Python instaladas una al lado de la otra. No hay inconveniente en instalar Python 3.7 y probarlo.

Ahora, para las preguntas más complicadas. ¿Deberías actualizar tu entorno de producción a Python 3.7? ¿Debería hacer que su propio proyecto dependa de Python 3.7 para aprovechar las nuevas funciones?

Con la salvedad obvio que siempre se debe hacer pruebas exhaustivas antes de actualizar el entorno de producción, hay muy pocas cosas en Python 3.7 que va a romper el código anterior ( asyncawaital convertirse en palabras clave es un ejemplo sin embargo). Si ya está usando un Python moderno, la actualización a 3.7 debería ser bastante fluida. Si quieres ser un poco conservador, es posible que desees esperar el lanzamiento de la primera versión de mantenimiento, Python 3.7.1, tentativamente esperada en algún momento de julio de 2018 .

Argumentando que debe hacer que su proyecto 3.7 solo sea más difícil. Muchas de las nuevas características en Python 3.7 están disponibles como backports para Python 3.6 (clases de datos importlib.resources) o como conveniencias (llamadas más rápidas al arranque y al método, depuración más sencilla y -Xopciones). Lo último, puede aprovechar ejecutando Python 3.7 usted mismo mientras mantiene su código compatible con Python 3.6 (o inferior).

Las características más importantes que bloquearán su código en Python 3.7 son los módulos __getattr__() , las referencias avanzadas en las sugerencias de tipo y las funciones de nanosegundostime . Si realmente necesita alguno de estos, debe seguir adelante y superar sus requisitos. De lo contrario, su proyecto probablemente será más útil para otros si se puede ejecutar en Python 3.6 por un tiempo más.

Consulte la guía Porting to Python 3.7 para obtener más información al actualizar.

Espero que este post te sea útil y que me dejes en los comentarios tu opinión sobre estas nuevas características de Python. Espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos y nos vemos la próxima.

Búsqueda binaria en Python

Búsqueda binaria en Python

Hola a todos, debido a que no quiero dejar de lado el blog durante largos periodos de tiempo, el día de hoy vengo a crear esta breve entrada, para dejar este vídeo que realicé, en donde explico el algoritmo de búsqueda binaria en Python.

búsqueda binaria en python

Ejemplo de búsqueda binaria

En este vídeo podremos apreciar una breve explicación de este algoritmo, viéndolo desde una perspectiva aplicada al lenguaje de programación Python, pero que con apenas esfuerzo, podremos realizarlo utilizando cualquier lenguaje. Esto gracias a que veremos el concepto del algoritmo.

Si tienes alguna duda o comentario no dudes en hacérmelo saber.

Espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos y nos vemos la próxima.

Manejar entornos virtuales en Python

Manejar entornos virtuales en Python

Hola, como podrás haber notado, ya he pasado un tiempo sin escribir en el sitio, varias cosas han sucedido en mi vida personal y eso me ha hecho enfocarme en otras cuestiones, pero como le tengo mucho cariño al sitio, se me ocurrió realizar esta entrada sobre un tema relativamente sencillo, pero bastante útil para aquellos desarrolladores Python novatos (y no tanto) que desean aumentar su productividad, y seguir las buenas prácticas de la industria. Por lo tanto veremos como manejar entornos virtuales en Python.

El problema de no manejar entornos virtuales en python

Como sabrás, Python cuenta con una sencilla administración de paquetes con la herramienta pip, y si no tenemos cuidado y organización a la hora de utilizarla, podemos caer en ciertas situaciones que no son de mucho agrado para nosotros como desarrolladores.

Por ejemplo, suponiendo que tenemos un proyecto (proyecto_1) que tiene como dependencia un módulo en su versión 1.9.12, pero otro proyecto (proyecto_2) que necesitamos para nuestro día a día, tiene la dependencia a ese mismo módulo en su versión 2.0.11, como es un cambio de versión mayor hay muchas incompatibilidades que nuestro proyecto_1 no puede manejar.

Esto nos pone en una situación donde nos debemos preguntar ¿qué versión del paquete instalo? ¿cómo utilizar una versión específica de ese módulo para un determinado proyecto? Estas eran unas de las tantas interrogantes que se hacían los administradores y desarrolladores hace un par de años, cuando se trataba de mantener aplicaciones Python, o cuando necesitaban probar si, la nueva versión de un determinado módulo era compatible con la aplicación en desarrollo.

Para darle solución a las preguntas anteriores y las situaciones que las originan, fue que se encontró la forma de trabajar con entornos virtuales. Pero…

¿A qué me refiero con un entorno virtual en Python?

No son más que un conjunto de herramientas que permiten crear entornos aislados, donde cada uno de ellos es responsable de sus propios paquetes. Es decir, nos permiten crear una sandbox en la que instalar diferentes módulos que serán independientes, tanto de los que existen globalmente en el sistema (dentro del directorio site-packages), como de otros entornos virtuales. Bastante interesante cierto, y espera a ver lo simple que es su manejo.

Creando nuestro primer entorno virtual en Python

Ficha técnica para esta entrada:

  • OS: Linux dev 4.15.9-1-ARCH x86_64 GNU/Linux.
  • Distribución: Arch Linux
  • Versión de Python: Python 3.6.4 (Compatible con Python 3.x y Python 2.x)
  • Intérprete de comandos: Zsh

Para llevar a cabo esta tarea, utilizaremos a virtualenv y virtualenvwrapper de los que hablaremos un poco más a continuación:

  • Virtualenv: básicamente es el motor de todo esto, es el módulo que nos permite realizar la gestión de entornos virtuales para python en nuestro sistema (creación, administración, elminación, etc), se encuentra disponible a tráves de PyPi, pero aunque cumple su objetivo, eran necesarios demasiados pasos en un proceso cansado para lograr tener nuestros entornos funcionando.
  • Virtualenvwrapper: con el objetivo de facilitar el manejo de entornos virtuales con Python, se desarrolló este paquete, que como su nombre indica, contiene una serie de herramientas que actúan como wrapper sobre virtualenv, y facilitan su utilización.

Entre las funcionalidades que incluye virtualenvwrapper, encontramos la de crear diferentes entornos virtuales bajo el mismo directorio del sistema de ficheros, activar un entorno a través de un nombre dado, permitir fácilmente desactivar un entorno virtual para activar y trabajar con otro diferente, borrar un entorno y copiar completamente un entorno con un nombre distinto.

¿Sigues sin estar convencido de su uso?

La instalación de virtualenvwrapper es muy sencilla, basta con ejecutar el comando:

pip install virtualenvwrapper

Esto instalará además las dependencias necesarias (incluyendo a virtualenv)

Antes de comenzar a utilizar virtualenvwrapper, es conveniente modificar nuestro archivo de configuración de nuestro intérprete de comandos (.bashrc, .profile, .zshrc o similar) para añadir un par de líneas que nos faciliten la gestión de nuestros entornos.

La primer línea indicará cuál será el directorio a partir del que se crearán los diferentes entornos si se desea utilizar un directorio diferente al establecido por default, el cuál está establecido en $HOME/.virtualenvs. La segunda línea se encargará de invocar automáticamente al shell script virtualenvwrapper.sh, cada vez que abramos una terminal, sin esto no podríamos utilizar las opciones para manejar nuestros entornos.


export WORKON_HOME=$HOME/.pythonenvs

source /usr/bin/virtualenvwrapper.sh

Ahora procedamos a crear nuestro primer entorno virtual con el siguiente comando:

mkvirtualenv nombredelentorno --no-site-packages

A detalle, basta con invocar al comando mkvirtualenv seguido del nombre que indiquemos que tenga nuestro entorno, además añadimos el parámetro –no-site-packages para asegurar que el entorno se encuentre aislado del directorio site-packages de nuestro sistema.

Por defecto, el comando anterior se encargará de crear nuestro entorno y además activarlo para comenzar a trabajar, esto podrás notarlo en tu intérprete de comandos, ya que posiblemente verás el nombre de tu nuevo entorno en el prompt, en mi caso se aprecia así, siendo (entornoprueba) el entorno virtual que acabo de crear:

(entornoprueba) ..[themushr00m@dev] - [~] - [lun mar 19, 05:54]

Con el comando lsvirtualenv podremos obtener la lista de los entornos virtuales que tenemos creados, y si queremos cambiar de un entorno a otro, podemos hacerlo con el comando workon seguido del nombre del entorno que queremos utilizar.

Este comando se encargará de desactivar el entorno activado actualmente, como de activar el nuevo. Si ningún entorno se encuentra activado, únicamente activará el entorno indicado.

Si lo que queremos es dejar de trabajar en un entorno virtual, podemos desactivarlo con el comando deactivate.

Este es un consejo, si desean que el parámetro –no-site-packages sea aplicado a todos los entornos virtuales que vayan creando, sin tener que estar escribiéndolo siempre, pueden hacer lo siguiente:

En su archivo .bashrc, .profile, .zshrc o similar, justo debajo de las dos líneas que añadimos anteriormente (esto para que tengamos orden) añadimos la siguiente línea:

export VIRTUALENVWRAPPER_VIRTUALENV_ARGS='--no-site-packages'

Y bien, con esto ya pueden comenzar a trabajar con entorno virtuales en Python, añadiendo paquetes a ellos sin ningún riesgo de afectar las versiones instaladas globalmente en el sistema (siempre deben asegurarse que su entorno virtual se encuentra activado :D)

Otra opción interesante es la de enlazar un entorno virtual determinado a un directorio de trabajo, esto nos permitirá que al activar dicho entorno, nos posicionemos en el directorio de trabajo inmediatamente, ¿genial cierto?

Esto lo realizamos de forma sencilla, únicamente es necesario que tengamos activo el entorno virtual que deseamos y nos posicionemos en el directorio que deseamos y ejecutamos el siguiente comando:

setvirtualenvproject

De esta forma, cada vez que abramos nuestro intérprete de comandos, y activemos el entorno virtual, nos posicionará inmediatamente en el directorio de trabajo del proyecto que definimos.

Si son unos talibanes de la documentación no olviden visitar el sitio de documentación de virtualenvwrapper para ver la referencia completa de comandos y parámetros.

Para cerrar con este tema de manejar entornos virtuales con Python, debo decir que pueden crear entornos virtuales con diferente versión de Python sin importar cual sea la principal de sus sistema, pero esto se los dejaré de tarea para que mediante la curiosidad puedan adentrarse a esta fabulosa herramienta.

Espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos y nos vemos la próxima.

Animaciones con Javascript: Tweens con GreenSock (GSAP)

animaciones con javascript

Hola a todos, estoy intentando no descuidar el blog, manteniendo mis actividades lo más organizadas posibles. Siguiendo la pauta que marqué con la entrada anterior intentaré que esta entrada sea lo más explicativa posible. Y bien, para no dar tantas vueltas sin sentido, en esta ocasión aprenderemos a crear animaciones con Javascript, para ello utilizaremos la librería GreenSock (GSAP).

animaciones con javascriptSeguro que en alguna página lo has visto en acción, o hayas escuchado sobre esta librería y ahora quieras meterte de lleno y comenzar a aprender su funcionamiento desde cero, ¿cierto? Si es tu caso, e incluso si no, deberías continuar leyendo, tal vez llegue a interesarte al final. 😉

Sigue leyendo

Código CSS Eficiente: Cuida el uso de IDs

código css eficiente

Hola a todos, esta ocasión también voy a escribir este post un poco corto, pero intentaré que sea lo más explicativo posible al problema. Planeo que se convierta en una serie de varias entradas que serán englobadas bajo el tema “Código CSS Eficiente“.

código css eficiente

Esta primera entrada hablaré sobre los IDs en los elementos HTML, y una situación “curiosa” con la que me encontré hace poco, siendo esta la premisa a mi recomendación de evitar en lo posible utilizarlos.

Sigue leyendo

Trabajando con WebSockets

Programación Python

Hola a todos, debido al trabajo he descuidado el blog estás últimas semanas, así que hoy decidí esforzarme en sacar tiempo y escribir este pequeño ejemplo de un tema qué alguien me preguntó hace unos días mediante Twitter.. En esta entrada llamada “Trabajando con WebSockets”, explicaré de forma breve pero concisa como trabajar con Web Sockets y JS.

A modo de ejemplo, crearemos lo más conocido para este tipo de “features”, un chat. Tal vez eres un dev que ha experimentado en el pasado con Socket.io y buscas saber un poco más sobre lo que hay por detrás, o simplemente estás intentando añadir carácteristicas “real-time” a tu app y no sabes por donde empezar, este post fue hecho pensando en ti. La aplicación final tendrá una apariencia como la siguiente:

Trabajando con WebSocketsLuce bien, ¿cierto? 😀 No sean tan duros, deben recordar que esto fue hecho en unos minutos, solo para poder ejemplificar la teoría, y poder centrarnos en lo realmente importante de este post.

¿Qué es un Web Socket?

Intentando ser simple, WebSocket es un protocolo nuevo para la web que funciona bajo TCP, por el cual, a diferencia de las conexiones HTTP (las que se utilizan normalmente en internet), este es bi-direccional, ¿que significa esto? Como te menciono, hoy por hoy en internet estamos usando una conexión en una sola dirección, solicitamos un recurso al servidor y esperamos la respuesta (o viceversa con ServerEvents, de los cuales planeo escribir pronto). Pero con web sockets el servidor te habla también, te puede llamar y mandar un mensaje en cualquier momento, cool ¿cierto?

Esto nos permite realizar cosas tan “mágicas” en nuestro sitios web, como podamos imaginar, siempre y cuando tengamos en cuenta algunos puntos importantes:

  • La comunicación se inicia con un handshake HTTP, por lo que si la conexión HTTP no se puede establecer, tampoco habrán WebSockets.
  • El soporte debe existir tanto en el cliente como en el servidor. (Tenemos HTML5 en el cliente y Socket.io o WS en el servidor 😀 )
  • A diferencia de los protocolos convensionales de TCP, donde podemos transmitir streams de bytes, en WebSockets solo se pueden transmitir texto/JSON.
  • Al igual que ocurre con HTTP, donde tenemos su versión segura (HTTPS), en los WebSockets contamos con la versión segura WSS, aunque debemos estar en el entendido que no todos los navegadores que soportan los WebSockets, soportarán WSS.

Lo interesante de los WebSockets es que nos dan este comportamiento “real-time”, que nos da la posibilidad de crear aplicaciones complejas de forma sencilla, ¿has querido hacer un juego multijugador? con los WS esto será algo “sencillo”.

Bien, muy interesante todo pero, ¿cómo funciona?

Trabajando con WebSocketsComo mencionaba anteriormente, para iniciar una conexión con el protocolo WS primero el cliente (el browser) le pide al servidor que quiere iniciar esta conexión (handshake: aquí se pasan unos datos secretos), si este proceso es satisfactorio, el servidor responde un “todo en orden, prosigue” y a partir de ahí dejan de utilizar HTTP y pasan a WS.

Además de darnos la posibilidad de una comunicación bi-direccional, también nos ahorramos el “payload”, es decir, esos bytes que se envían en las transmisiones que hacemos por HTTP, que contienen la información para que tanto el cliente, como el servidor sepan que estan recibiendo, qué tipo de conexión utilizamos, etc. Este “payload” es mejor conocido como “cabeceras HTTP”. Esto ya que recordemos que únicamente podemos transmitir texto y nos quedamos “conectados” al servidor hasta que nosotros (los clientes) decidamos que no queremos seguir conectados.

Pequeño ejemplo: Trabajando con WebSockets

Bien, tal vez tanta teoría te abruma, así que aquí te dejo el código del ejemplo para que puedas reproducirlo. y ver un poco de que se trata. Como el código cuenta con comentarios, voy a dejar implicita la explicación del mismo por esta ocasión (es un post express) 🙂

Primeramente y ya teniendo un proyecto creado, instalamos el paquete wsdesde NPM.

yarn add ws

Teniendo el siguiente index.html:


<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, maximum-scale=1.0">
    <title>Simple Chat App - VueJS and Web Sockets</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/lumen/bootstrap.min.css">
    <style>
        .container {margin-top: 40px;}
        #messages { height: auto; min-height: 150px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="col-xs-12 col-md-offset-3 col-md-6">
            <div class="panel panel-primary">
                <div class="panel-heading">Join the conversation!</div>
                <div class="panel-body">
                    <div class="form-group">
                        <label for="messages" class="form-control-label">Messages</label>
                        <div id="messages" class="form-control"></div>
                    </div>
                    <div class="form-inline">
                        <div class="form-group form-group-sm">
                            <label for="message_text" class="form-control-label">Write your message:</label>
                            <input type="text" class="form-control" id="message_text" />
                        </div>
                        <button class="btn btn-primary">Send</button>
                    </div>
                </div>
            </div>
            <p id="username-label"></p>
        </div>
    </div>

    <script src="app.js"></script>
</body>
</html>

Vemos que llama un script, cuyo contenido es el siguiente:


// app.js
// URL dónde se encuentra ejecutando nuestro servidor de WS
const wsUri = "ws://localhost:5001"
// Creamos un nuevo "cliente" de WS
let socket = new WebSocket(wsUri)
// Ya que la aplicación de ejemplo es una sala de chat
// al iniciar nos pide un nombre de usuario para utilizarlo
const name = prompt("What is your username?")
document.getElementById('username-label').innerText = `You are: ${name}`

// Controla la conexión al WS
socket.onopen = event => {
  // Descomentar para ver que recibimos por parte del WS
  //console.log(event)
  socket.send(JSON.stringify({
    type: "name",
    data: name
  }))
  console.log("Socket connected successfully...")
}

let messages = document.getElementById("messages")

// Nos suscribimos a los eventos de transferencia de mensajes
// por parte del servidor, si tuvieramos mensajes de diferentes tipos
// podríamos llevar a cabo una validación
socket.onmessage = event => {
  // Descomentar para ver que recibimos por parte del WS
  //console.log(event)
  const data = JSON.parse(event.data)
  messages.innerHTML += `<span class="text-success">${data.name}:</\span> ${data.data}<br /\>`
}

document.querySelector(".btn").onclick = event => {
  const text = document.getElementById("message_text").value
  socket.send(JSON.stringify({
    type: "message",
    data: text
  }))
  messages.innerHTML += `<span class="text-warning">You:</\span> ${text}<br /\>`
}

Creamos nuestro archivo server.js, que será el encargado de ejecutar el servidor con soporte para WS.


// server.js
// Referenciamos las librerías a utilizar

// Para manejar los WebSockets utilizaremos la librería 'ws'
// que nos facilita bastante el trabajo con estos.
const WS = require("ws").Server
// Puerto a donde escuchará nuestro servidor WS
const port = 5001
// Creamos nuestro servidor de WS
const server = new WS({ port })

// Nos suscribimos al evento de conexión
// el cual es llamado cuando un cliente se conecta
server.on("connection", ws => {
  // El método callback es llamado cuando
  // se conecta un nuevo cliente y en el argumento "ws"
  // vamos a tener un "enlace" a este cliente

  // Podemos enviar un mensaje al cliente
  // de bienvenida apenas se conecte
  // los parámetros son el nombre del mensaje y un json con los datos
  //ws.send('welcome', { greeting: 'Welcome WS client!' })

  // Los WebSockets se comunican en base a mensajes
  // por lo que debemos suscribirnos a cada uno de los
  // posibles mensajes que los clientes puedan enviar
  ws.on("message", message => {
    console.log(`Received: ${message}`)
    message = JSON.parse(message)
    // Ya que el cliente puede enviar mensajes de diferentes tipos
    // Validamos que el mensaje recibido sea del tipo "name"
    if(message.type === "name") {
      // Store the username that send the email
      ws.userName = message.data
      return
    }

    for(let client of server.clients) {
      // Cómo está será una aplicación de chat
      // enviamos el mensaje a todos los clientes
      // evitando enviarlo a "nosotros"
      if(client !== ws)
        client.send(JSON.stringify({
          type: "message",
          name: ws.userName,
          data: message.data
        }))
    }
  })

  // Nos suscribirnos también al evento
  // que se ejcuta cuando un cliente decide terminar la conexión
  ws.on("close", event => {
    console.log("A client was disconnected...")
  })

  // Todos los llamados a console.log son del servidor
  // con própositos de depuración
  console.log("New client connected...")
})

Ahora únicamente debemos ejecutar el servidor

node server.js

Tan sólo abrimos nuestro index.html en cualquier navegador que soporte WS (Chrome, Firefox, etc) y podremos ver nuestra aplicación funcionando.

Al principio expliqué que tanto el cliente como el servidor deben soportar web sockets, tenemos que tener en cuenta que es necesario HTML5 con web sockets, te dejo este enlace a Caniuse.com con el detalle de los navegadores que lo soportan.

Opciones como SocketIO se encargan de que la conexión funcione sin importar el navegador, esto lo hace intentando con otros mecanismos de transporte (XHR Pooling, Flash Sockets, etc.). No va a ser con web sockets pero va a funcionar como si lo fuera.

Creo que es todo por ahora, si gustan saber más al respecto de “ws”, pueden visitar su documentación, que siendo honestos es bastante clara. Recuerda que todo el código fuente puedes encontrarlo en el siguiente repositorio.

Espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Crear APIs REST con Python y Hug

Hug framework

Hola a todos, han sido unas semanas muy divertidas, en las cuales he ido aprendiendo a ser padre, por lo que deje un poco olvidado el blog; pero creí conveniente dedicarle un tiempo. En esta entrada, vamos a aprender a crear APIs REST con Python y Hug, un framework que llevo un tiempo utilizando en mis proyectos, y que me ha dejado un buen sabor de boca, debido a su rendimiento y facilidad para obtener productos finales.

Sigue leyendo

Tipos estáticos en Python con Mypy

tipos estáticos en python

Tipos estáticos en Python

Hola a todos, en esta entrada vamos a hablar sobre los tipos estáticos en Python, utilizando Mypy, esta herramienta desarrollada principalmente por un equipo de Dropbox, incluyendo al creador del lenguaje Guido Van Rossum.

tipos estáticos en python

Tipado estático en lenguajes dinámicos

Desde hace un par de años, han salido a la luz verificadores de tipado estático para lenguajes dinámicos populares como Hack para PHP o Flow y TypeScript para Javascript, los cuales han sido ampliamente adoptados por la comunidad (yo soy un feliz usuario de Flow).

Hace unos años tres años, una sintaxis provisional de anotaciones para tipos estáticos fue añadida a Python 3, sin embargo, los tipos estáticos en Python no habían sido adoptados aún ampliamente, debido a que no existía una herramienta para llevar a cabo la verificación. Es aquí donde llegamos a Mypy.

¿Qué es Mypy?

Como podemos encontrar en su página oficial:

Mypy is an experimental optional static type checker for Python that aims to combine the benefits of dynamic (or “duck”) typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking. Mypy type checks standard Python programs; run them using any Python VM with basically no runtime overhead.

Cabe decir que a pesar de que Mypy ya soporta la mayoría de las características de Python, aún se encuentra en desarrollo. Y como es bien sabido, nada como un ejemplo para ver de que estamos hablando, también sacado de la página oficial de Mypy.

tipos estáticos en PythonSe puede observar en la imagen la diferencia al utilizar el tipado estático que Mypy nos permite, como mencionaba en las líneas de arriba, se utiliza la sintaxis de anotaciones para tipos estáticos de Python, logrando con esto no afectar el rendimiento de nuestros scripts ni nos hace añadir códigos extraños y dotándonos de los beneficios de tener tipado estático en un lenguaje flexible y dinámico como lo es Python.

Instalando Mypy

Para instalar esta herramienta, debemos tener Python >= 3.3 y desde nuestra línea de comandos debemos ejecutar lo siguiente:


pip install mypy

Empezando con Mypy

Con Mypy contamos con dos tipos de anotaciones diferentes, la mencionada anteriormente (la sintaxis de anotaciones de tipos estáticos de Python) y la sintaxis utilizando comentarios. Veamos un ejemplo:

# Primero desde el módulo typing debemos
# importar los tipos que vamos a necesitar
from typing import List


# Utilizando la sintaxis de anotaciones
def sum_list_to_str(nums: List[int]) -> str:
    """Suma los números de una lista, y retorna el resultado como string"""
    return str(sum(nums))


# Utilizando comentarios
def sum_list_to_str_2(nums):
    # type: (List[int]) -> str
    """Suma los números de una lista, y retorna el resultado como string"""
    return str(sum(nums))

Para ejecutar los scripts con anotaciones de Mypy, podemos realizarlo utilizando cualquier intérprete de Python, esto se debe a que Mypy comparte esta excelente propiedad con el verficador de código de Javascript, Flow. Esto es grandioso ya que significa que podemos añadir Mypy a nuestros proyectos sin preocuparnos de como debemos correr estos proyectos con Python.

De igual manera, podemos utilizar Mypy como un linter, el cual puede mostrar nuestros errores en un formato agradable. Por ejemplo, vamos a cambiar el valor de retorno en nuestra segunda función para comprobar esto, quedando de la siguiente manera:

# Utilizando comentarios
def sum_list_to_str_2(nums):
    # type: (List[int]) -> int
    """Suma los números de una lista, y retorna el resultado como string"""
    return str(sum(nums))

Ahora, desde nuestra línea de comandos ejecutamos:


mypy mi_archivo.py

Output: mi_archivo.py:16: error: Incompatible return value type (got "str", expected "int")

Genial, ¿no crees? Gracias a esto será muy sencillo depurar nuestros scripts y evitar errores durante la ejecución que muchas veces se vuelven una pesadilla para reparar. Si te interesa profundizar sobre las anotaciones de tipo de Mypy, puedes consultar el Cheat Sheet de Mypy y también en PEP-484.

En resumen…

Utilizar Mypy es una excelente opción cuando:

  • Buscamos utilizar tipos estáticos en Python.
  • Verificar tipos en “tiempo de compilación”
  • Lograr un mantenimiento sencillo.
  • Buscamos que nuestro código sea simple de entender y más sencillo de modificar sin introducir “bugs”
  • Llevar nuestros scripts de dinámicos a estáticos.
  • Desarrollar nuestros scripts con tipado dinámico e ir añadiendo tipado estático conforme el código vaya madurando, o migrar código Python existente a tipado estático.
  • Declaraciones de tipo que actúen como documentación verificada por máquina.

Y bien, es todo por esta ocasión, espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos.

Crear un blog con NodeJS y Hexo

crear un blog con nodejs

Crear un blog con NodeJS

Hola a todos, en esta entrada vengo a escribir sobre Hexo, una herramienta de línea de comandos que nos permitirá crear un blog con nodejs, y con esto, crear la estructura de un blog de forma muy sencilla.

crear un blog con nodejs

¿Qué es Hexo?

Bien, como en su propia página de documentación nos adelantan:

Hexo is a fast, simple and powerful blog framework. You write posts in Markdown (or other languages) and Hexo generates static files with a beautiful theme in seconds.

Como podemos apreciar Hexo nos permitirá crear un blog de una forma sencilla y rápida. Nos da la posibilidad de crear los post de nuestro blog utilizando Markdown (lo cual es grandioso) o algún otro lenguajes y generar archivos estáticos y con un hermoso tema en segundos.

Generando nuestro blog

Lo primero que debemos hacer es instalar hexo, para esto debemos tener instalado NodeJS previamente, y ejecutar alguno de los siguientes comandos:


# Utilizando npm

npm install -g hexo-cli

# Utilizando yarn (recomendado)

yarn global add hexo-cli

Con esto ya tendremos el binario de hexo listo para utilizar, ahora lo que debemos hacer es ejecutar el siguiente comando:


hexo init my_blog

Luego nos posicionamos en el directorio que nos ha creado el comando anterior:


cd my_blog

Instalamos las dependencias:


# Utilizando npm

npm install

# Utilizando yarn

yarn install

Si todo se instaló correctamente, simplemente ejecutamos el servidor de hexo:


hexo server

Ahora si navegamos a la url que nos muestra en la salida (http://localhost:4000/), podremos visualizar nuestro blog, que debería verse parecido a lo siguiente:

crear un blog con nodejsSencillo y rápido, ¿no? Cumplen lo que prometen 😀

Creando un post

Tal vez, te estás preguntando, ¿cómo se crean los post?

La respuesta es simple. Únicamente debemos ejecutar el siguiente comando:


hexo new post 'Titulo de tu post'

Donde debemos reemplazar ‘Titulo de tu post’ con el titulo que quieras que tenga tu post. Para realizar esto no es necesario que detengas el servidor de hexo. Y listo, solo debes comenzar a escribir el contenido de tu post en el archivo generado con el titulo que escribiste dentro de la carpeta source.

Y deberías tener un resultado como el siguiente:

crear un blog con nodejsSi desean saber más sobre este grandioso framework siempre pueden darle un vistazo a su buena documentación, que aunque aún tiene mucho por delante, es una buena opción si quieres algo simple y que te permita llevar a cabo el objetivo de un blog: escribir.

Y bien, es todo por esta ocasión, espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos.

¿Qué es __name__ en Python?

¿Qué es __name__ en Python?

Hola a todos, ya llevo un rato sin escribir, esto se debe a que me encuentro pasando por muchas cosas personales, muy buenas, y me estoy enfocando en ellas, pero sin embargo tengo muchos borradores que espero ya terminar y publicar, como adelanto les puedo decir que tratan sobre Xamarin, Vue, Python y Ruby. Y bien, bienvenidos a esta entrada donde a petición de un usuario en FB voy a intentar explicar qué es __name__ en Python.

que es __name__ en python

Seguro que al estar leyendo un libro sobre Python, o leyendo el código de algún script de tu interés has visto algo como lo siguiente:


class MyComplexClass(object):
    def __init__(self):
    print("Hello World!")
    # Código de la clase con lógica demasiado compleja
    # ...

def init():
    my_complex = MyComplexClass()
    # Resto de la lógica del script
    # ...

if __name__ == '__main__':
    main()

Si eres curioso, o el libro donde viste esto es de un buen autor, seguro explicaron de que se trata, si no es tu caso, o buscando sobre el tema diste con el blog sigue leyendo, no te defraudaré 🙂

Pequeño paréntesis cultural

Muchos lenguajes de programación, normalmente los lenguajes compilados (llámese C, Java, Go, Rust, etc.), requieren de una función que actúe como punto de entrada, esto es, que será lo primero en ejecutarse al correr el programa. Comúnmente esta función de punto de entrada es main() (Espero haber sido breve).

Sigo sin saber qué es __name__ en Python

Lo sé, no desesperes, las líneas anteriores eran necesarias. Python, al igual que muchos de los lenguajes de script, ejecuta el código interpretando cada línea de arriba a abajo. Cuando el intérprete de Python encuentra el bloque if anterior (if __name__ == ‘__main__’), comprueba el atributo especial __name__, con el objetivo de ver si su valor actual es “__main__”.

Tiene sentido…al cumplirse esta condición, estaremos seguros de que nuestro módulo (script) es el primero en ser “cargado” por el intérprete de Python. Suponiendo que el código anterior esté almacenado en el archivo mi_super_script.py y lo ejecutamos:


python mi_super_script.py

El resultado será lo que este definido dentro de la función init(). Por lo tanto, aunque Python no especifica como tal, oficialmente una función de punto de entrada que será ejecutada en primer lugar, el bloque anterior es una forma “idiomática” de lograr el mismo objetivo.

Por lo tanto, con esto nos aseguramos de que ese código en especifico será ejecutado únicamente cuando nuestro script sea ejecutado, y no cuando sea importado. ¿A qué me refiero?


# mi_super_script.py
def main():
    print("Script 1!")

if __name__ == '__main__':
    main()

# mi_super_script_dos.py
import .mi_super_script

def init():
    print("Script 2!")

if __name__ == '__main__':
    init()

Si nosotros ejecutaramos ambos scripts, tendríamos la siguiente salida:


python mi_super_script.py

# Output: "Script 1!"

python mi_super_script_dos.py

# Ouput: "Script 2!"

Como podrás notar, a pesar de que en mi_super_script_dos.py estamos importando el código de mi_super_script.py la función main no ha sido ejecutada.

Generalmente no es útil sobreescribir el valor por defecto que el intérprete le otorga a __name__, pero existe un caso puntual donde esto se lleva a cabo con frecuencia: funciones decoradoras.

Voy a dejar ese tema para otra entrada si les interesa la relación que tienen con el atributo especial __name__ les sugiero leer esto y esto.

Y bien, es todo por esta ocasión, espero la explicación haya sido clara y cualquier comentario no dudes en dejarlo aquí abajo, o con confianza mencionalo por Twitter o en la Fanpage de Facebook, y aprovecho a invitarte para que me sigas en las redes y compartas el contenido del blog, con lo cual me harías un gran favor.

Saludos.