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 pdb
y 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 pudb
por 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.py
a:
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()
y 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__()
y .__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 Country
como 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;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;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;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;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 .coastline
se utilizan cuando se realiza la inicialización de la clase (aunque .coastline
es opcional, como se muestra en el ejemplo de litoral Nepal). La clase Country
tiene un repr
mé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 repr
y 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.attr
primero buscarán attr
definidas 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, plugins
y 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:
- En primer lugar, la función intenta devolver de manera optimista el plugin nombrado del diccionario
PLUGINS
. Esto tendrá éxito si existename
un plugin llamado y ya ha sido importado. - Si el plugin indicado no se encuentra en el diccionario
PLUGINS
, nos aseguramos de que se importen todos los plugins. - Devuelve el plugin indicado si está disponible después de la importación.
- Si el plugin no está en el diccionario
PLUGINS
después de importar todos los plugins, levantamos unAttributeError
diciendo quename
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.resources
será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;gt; None: self.left = left self.right = right
La ejecución del código genera una NameError
porque 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;lt;module&amp;amp;amp;gt; class Tree: File "tree.py", line 2, in Tree def __init__(self, left: Tree, right: Tree) -&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;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;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 None
porque 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()
o 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 especificadoclock_settime_ns()
: Establece el tiempo de un reloj especificadomonotonic_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 cortosprocess_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 _ns
sufijo. 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 int
es 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 _ns
versión. La resolución de time.time_ns()
es aproximadamente 3 veces mejor que para time.time()
.
¿Qué es un nanosegundo por cierto? Técnicamente, es una milmillonésima de segundo, o 1e-9
segundo si prefieres la notación científica. Sin embargo, estos son solo números y realmente no proporcionan ninguna intuición. Para una mejor ayuda visual, lee la maravillosa demostración de Grace Hopper del nanosegundo .
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 async
y await
. Para evitar problemas de compatibilidad con versiones anteriores, async
y await
no se agregaron a la lista de palabras clave reservadas. En otras palabras, aún era posible definir variables o funciones nombradas async
y await
.
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_resources
para 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.txt
de 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.resources
en Python 2.7 y Python 3.4+ a través de un backport . Una guía sobre la migración de pkg_resources
queimportlib.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 -X
opciones 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 plugins
duró 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 dev
para 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 faulthandler
de mostrar un rastreo en bloqueos serios, así como más advertencias y enlaces de depuración.
Finalmente, -X utf8
habilita el modo UTF-8 . (Consulte PEP 540 ). En este modo, UTF-8
se 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
typing
es 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 Anaconda o pyenv
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 ( async
y await
al 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 -X
opciones). 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.
Deja un comentario