Presentamos a su atención la segunda parte de la traducción de material dedicado a las características de trabajar con módulos en proyectos de Instagram Python. La
primera parte de la traducción ofreció una visión general de la situación y mostró dos problemas. Uno de ellos se refiere al inicio lento del servidor, el segundo, los efectos secundarios de los comandos de importación inseguros. Hoy esta conversación continuará. Consideraremos otro problema y hablaremos sobre los enfoques para resolver todos los problemas planteados.

Problema 3: estado global mutable
Eche un vistazo a otra categoría de errores comunes.
def myview(request): SomeClass.id = request.GET.get("id")
Aquí estamos en la función de presentación y adjuntamos el atributo a una determinada clase en función de los datos recibidos de la solicitud. Probablemente ya entendiste la esencia del problema. El hecho es que las clases son singletones globales. Y aquí ponemos el estado, según la solicitud, en un objeto de larga vida. En un proceso de servidor web que tarda mucho en completarse, esto puede generar una contaminación de cada solicitud futura que se realice como parte de este proceso.
Lo mismo puede suceder fácilmente en las pruebas. En particular, en los casos en que los programadores intentan usar
parches de mono y no usan
el administrador de contexto , como
mock.patch
. Esto puede conducir no a la contaminación de las solicitudes, sino a la contaminación de todas las pruebas que se realizarán en el mismo proceso. Esta es una razón seria para el comportamiento poco confiable de nuestro sistema de prueba. Este es un problema importante y es muy difícil evitarlo. Como resultado, abandonamos el sistema de prueba unificado y cambiamos a un esquema de aislamiento de prueba, que puede describirse como "una prueba por proceso".
En realidad, este es nuestro tercer problema. Un estado global mutable es un fenómeno no exclusivo de Python. Lo puedes encontrar en cualquier lugar. Estamos hablando de clases, módulos, listas o diccionarios adjuntos a módulos o clases, sobre objetos únicos creados a nivel de módulo. Trabajar en tal ambiente requiere disciplina. Para evitar la contaminación del estado global mientras el programa se está ejecutando, necesita un conocimiento muy bueno de Python.
Introduciendo módulos estrictos
Una de las causas principales de nuestros problemas puede ser que usemos Python para resolver problemas para los que este lenguaje no está diseñado. En equipos pequeños y proyectos pequeños, si sigue las reglas cuando usa Python, este lenguaje funciona bien. Y deberíamos ir a un lenguaje más riguroso.
Pero nuestra base de código ya ha superado el tamaño que nos permite al menos hablar sobre cómo reescribirlo en otro idioma. Y, lo que es más importante, a pesar de todos los problemas que enfrentamos, Python tiene mucho que ver con eso. Nos da más bien que mal. A nuestros desarrolladores realmente les gusta este lenguaje. Como resultado, depende solo de nosotros cómo hacer que Python funcione en nuestra escala y cómo asegurarnos de que podamos continuar trabajando en el proyecto a medida que se desarrolla.
Encontrar soluciones a nuestros problemas nos llevó a una idea. Consiste en utilizar módulos estrictos.
Los módulos estrictos son módulos de Python de un nuevo tipo, al principio del cual hay una construcción
__strict__ = True
. Se implementan utilizando muchos de los mecanismos de extensibilidad de bajo nivel que Python ya tiene. Un
cargador de módulo especial analiza el código usando el módulo
ast
, realiza una interpretación abstracta del código cargado para analizarlo, aplica varias transformaciones al AST y luego lo vuelve a
compile
en el código de bytes de Python usando la función de
compile
incorporada.
Sin efectos secundarios de importación
Los módulos estrictos imponen algunas restricciones sobre lo que puede suceder a nivel de módulo. Por lo tanto, todo el código de nivel de módulo (incluidos los decoradores y las funciones / inicializadores llamados a nivel de módulo) debe estar limpio, es decir, el código está libre de efectos secundarios y no utiliza mecanismos de E / S. El intérprete abstracto verifica estas condiciones utilizando los medios del análisis de código estático en tiempo de compilación.
Esto significa que el uso de módulos estrictos no causa efectos secundarios al importarlos. El código ejecutado durante la importación del módulo ya no puede causar problemas inesperados. Debido al hecho de que probamos esto a nivel de interpretación abstracta, usando herramientas que comprenden un gran subconjunto de Python, eliminamos la necesidad de limitar excesivamente la expresividad de Python. Muchos tipos de código dinámico, desprovistos de efectos secundarios, se pueden utilizar de forma segura a nivel de módulo. Esto incluye varios decoradores y la definición de constantes de nivel de módulo mediante listas o generadores de diccionario.
Hagámoslo más claro, consideremos un ejemplo. Aquí está el módulo estricto escrito correctamente:
"""Module docstring.""" __strict__ = True from utils import log_to_network MY_LIST = [1, 2, 3] MY_DICT = {x: x+1 for x in MY_LIST} def log_calls(func): def _wrapped(*args, **kwargs): log_to_network(f"{func.__name__} called!") return func(*args, **kwargs) return _wrapped @log_calls def hello_world(): log_to_network("Hello World!")
En este módulo, podemos usar las construcciones habituales de Python, incluido el código dinámico, uno que se usa para crear el diccionario y otro que describe el decorador de nivel de módulo. Al mismo tiempo, acceder a los recursos de red en las funciones
_wrapped
o
hello_world
es completamente normal. El hecho es que no se llaman a nivel de módulo.
Pero si
log_to_network
llamada
log_to_network
a la función externa
log_calls
, o si intentáramos usar un decorador que causara efectos secundarios (como
@route
del ejemplo anterior), o si
hello_world()
llamada
hello_world()
a nivel de módulo, entonces dejaría de ser estrictamente estricto -módulo
¿Cómo descubrir que no es seguro llamar a
log_to_network
o
route
funciones a nivel de módulo? Partimos del supuesto de que todo lo importado de módulos que no son módulos estrictos es inseguro, con la excepción de algunas funciones de la biblioteca estándar que se sabe que son seguras. Si el módulo
utils
es un módulo estricto, entonces podemos confiar en el análisis de nuestro módulo para informarnos si la función
log_to_network
es
log_to_network
.
Además de mejorar la confiabilidad del código, las importaciones que no tienen efectos secundarios eliminan una barrera seria para asegurar descargas incrementales de código. Esto abre otras posibilidades para explorar formas de acelerar los equipos de importación. Si el código de nivel del módulo no tiene efectos secundarios, esto significa que podemos ejecutar de forma segura las instrucciones individuales del módulo en modo "diferido", a pedido, al acceder a los atributos del módulo. Esto es mucho mejor que seguir el algoritmo "codicioso", en cuya aplicación se ejecuta de antemano todo el código del módulo. Y, teniendo en cuenta el hecho de que la forma de todas las clases en el módulo estricto es completamente conocida en el momento de la compilación, en el futuro incluso podemos tratar de organizar el almacenamiento permanente de los metadatos del módulo (clases, funciones, constantes) generados por la ejecución del código. Esto nos permitirá organizar la importación rápida de módulos sin cambios, lo que no requiere la ejecución repetida de bytecode del nivel del módulo.
Inmunidad y atributo __slots__
Los módulos y clases estrictos declarados en ellos son inmutables después de su creación. Los módulos se vuelven inmutables con la ayuda de la transformación interna del cuerpo del módulo en una función en la que el acceso a todas las variables globales se organiza a través de las variables de cierre. Estos cambios han reducido seriamente la posibilidad de un cambio aleatorio en el estado global, aunque el estado global mutable aún puede resolverse si se decide usarlo a través de módulos de nivel de contenedor mutable.
Los miembros de las clases declaradas en módulos estrictos también deben declararse en
__init__
. Se escriben automáticamente en el atributo
__slots__
durante la transformación AST realizada por el cargador de módulos. Como resultado, más tarde ya no puede adjuntar atributos adicionales a la instancia de clase. Aquí hay una clase similar:
class Person: def __init__(self, name, age): self.name = name self.age = age
Durante la transformación AST, que se realiza durante el procesamiento de módulos estrictos, se detectarán las operaciones de asignación de valores al
name
y
age
atributos de
age
realizados en
__init__
, y se adjuntará a la clase un atributo de la forma
__slots__ = ('name', 'age')
. Esto evitará que se agreguen otros atributos a la instancia de clase. (Si se utilizan anotaciones de tipo, tenemos en cuenta la información sobre los tipos disponibles en el nivel de clase, como
name: str
, y también los agregamos a la lista de espacios).
Las limitaciones descritas no solo hacen que el código sea más confiable. Ayudan a acelerar la ejecución del código. La transformación automática de clases con la adición del atributo
__slots__
aumenta la eficiencia del uso de memoria cuando se trabaja con estas clases. Esto le permite deshacerse de las búsquedas de diccionario cuando trabaja con instancias individuales de clases, lo que acelera el acceso a los atributos. Además, podemos continuar optimizando estos patrones durante la ejecución del código Python, lo que nos permitirá mejorar aún más nuestro sistema.
Resumen
Los módulos estrictos siguen siendo tecnología experimental. Tenemos un prototipo funcional, estamos en las primeras etapas de implementación de estas capacidades en producción. Esperamos que después de obtener suficiente experiencia en el uso de módulos estrictos, podamos hablar más sobre ellos.
Estimados lectores! ¿Crees que las características que ofrecen los módulos estrictos son útiles en tu proyecto de Python?
