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.