python scope raii with-statement

RAII en Python-destrucción automática cuando se deja un alcance



scope with-statement (5)

Pero RAII también trabaja con las reglas de alcance de C ++ para garantizar la destrucción inmediata del objeto.

Esto se considera poco importante en los idiomas de GC, que se basan en la idea de que la memoria es fungible . No es necesario presionar para recuperar la memoria de un objeto siempre que haya suficiente memoria en otro lugar para asignar nuevos objetos. Los recursos no fungibles, como los manejadores de archivos, sockets y mutexes, se consideran un caso especial que debe tratarse especialmente (por ejemplo, with ). Esto contrasta con el modelo de C ++ que trata a todos los recursos de la misma manera.

Tan pronto como la variable salga de la pila se destruye.

Python no tiene variables de pila. En términos de C ++, todo es un shared_ptr .

Python tiene cierto alcance, pero no en el nivel de sangría, solo en el nivel funcional. Parece una tontería requerir que haga una nueva función solo para definir las variables y poder reutilizar un nombre.

También realiza el alcance al nivel de comprensión del generador (y en 3.x, en todas las comprensiones).

Si no quiere obstruir sus variables de bucle for , no use tantas for bucles. En particular, no es Pythonic para usar append en un bucle. En lugar de:

new_points = [] for x,y,z in surface.points: ... # Do something with the points new_points.append( (x,y,z) )

escribir:

new_points = [do_something_with(x, y, z) for (x, y, z) in surface.points]

o

# Can be used in Python 2.4-2.7 to reduce scope of variables. new_points = list(do_something_with(x, y, z) for (x, y, z) in surface.points)

He estado tratando de encontrar RAII en Python. La asignación de recursos es Inicialización es un patrón en C ++ por el cual un objeto se inicializa a medida que se crea. Si falla, entonces lanza una excepción. De esta manera, el programador sabe que el objeto nunca se dejará en un estado a medio construir. Python puede hacer esto mucho.

Pero RAII también trabaja con las reglas de alcance de C ++ para garantizar la destrucción inmediata del objeto. Tan pronto como la variable salga de la pila se destruye. Esto puede suceder en Python, pero solo si no hay referencias externas o circulares.

Más importante aún, un nombre para un objeto todavía existe hasta que la función está en salidas (y, a veces, más). Las variables en el nivel del módulo se mantendrán durante la vida útil del módulo.

Me gustaría recibir un error si hago algo como esto:

for x in some_list: ... ... 100 lines later ... for i in x: # Oops! Forgot to define x first, but... where''s my error? ...

Podría eliminar manualmente los nombres después de usarlos, pero eso sería bastante feo y requeriría un esfuerzo por mi parte.

Y me gustaría que hiciera lo que quiero decir en este caso:

for x in some_list: surface = x.getSurface() new_points = [] for x,y,z in surface.points: ... # Do something with the points new_points.append( (x,y,z) ) surface.points = new_points x.setSurface(surface)

Python tiene cierto alcance, pero no en el nivel de sangría, solo en el nivel funcional. Parece una tontería requerir que haga una nueva función solo para definir las variables y poder reutilizar un nombre.

Python 2.5 tiene la declaración "con", pero eso requiere que coloque explícitamente las funciones __enter__ y __exit__ y, en general, parece estar más orientado a limpiar recursos como archivos y bloqueos de __enter__ __exit__ independientemente del vector de salida. No ayuda con el alcance. ¿O me estoy perdiendo algo?

Busqué "Python RAII" y "Python scope" y no pude encontrar nada que tratara el problema de forma directa y autoritaria. He revisado todos los PEPs. El concepto no parece ser abordado dentro de Python.

¿Soy una mala persona porque quiero tener variables de alcance en Python? ¿Es eso demasiado poco-Pythonic?

¿No lo estoy tragando?

Tal vez estoy tratando de quitar los beneficios de los aspectos dinámicos del lenguaje. ¿Es egoísta querer a veces aplicar un alcance?

¿Soy perezoso por querer que el compilador / intérprete detecte mis errores de reutilización de variables negligentes? Bueno, sí, por supuesto que soy perezoso, pero ¿soy perezoso de mala manera?


  1. Tiene razón al respecto: no tiene relación alguna con el alcance variable.

  2. Evita las variables globales si crees que son un problema. Esto incluye variables de nivel de módulo.

  3. La principal herramienta para ocultar el estado en Python son las clases.

  4. Las expresiones generadoras (y en Python 3 también enumera las comprensiones) tienen su propio alcance.

  5. Si sus funciones son lo suficientemente largas como para perder el rastro de las variables locales, probablemente debería refactorizar su código.


Al cambiar a Python después de años de C ++, me pareció tentador confiar en __del__ para imitar el comportamiento de tipo RAII, por ejemplo, para cerrar archivos o conexiones. Sin embargo, hay situaciones (p. Ej., Patrón de observador implementado por Rx) donde la cosa observada mantiene una referencia a su objeto, ¡lo mantiene vivo! Por lo tanto, si desea cerrar la conexión antes de que sea terminada por la fuente, no llegará a ninguna parte al intentar hacerlo en __del__ .

La siguiente situación surge en la programación de la interfaz de usuario:

class MyComponent(UiComponent): def add_view(self, model): view = TheView(model) # observes model self.children.append(view) def remove_view(self, index): del self.children[index] # model keeps the child alive

Por lo tanto, aquí hay una manera de obtener un comportamiento de tipo RAII: crear un contenedor con los enlaces de agregar y eliminar

import collections class ScopedList(collections.abc.MutableSequence): def __init__(self, iterable=list(), add_hook=lambda i: None, del_hook=lambda i: None): self._items = list() self._add_hook = add_hook self._del_hook = del_hook self += iterable def __del__(self): del self[:] def __getitem__(self, index): return self._items[index] def __setitem__(self, index, item): self._del_hook(self._items[index]) self._add_hook(item) self._items[index] = item def __delitem__(self, index): if isinstance(index, slice): for item in self._items[index]: self._del_hook(item) else: self._del_hook(self._items[index]) del self._items[index] def __len__(self): return len(self._items) def __repr__(self): return "ScopedList({})".format(self._items) def insert(self, index, item): self._add_hook(item) self._items.insert(index, item)

Si UiComponent.children es un ScopedList , en el que las llamadas acquire y ScopedList métodos en los niños, usted obtiene la misma garantía de adquisición y disposición determinista de recursos que está acostumbrado en C ++.


Básicamente estás usando el lenguaje equivocado. Si desea reglas de alcance sanas y destrucción confiable, entonces quédese con C ++ o pruebe Perl. El debate de GC sobre cuándo se libera la memoria parece no entenderse. Se trata de liberar otros recursos, como mutexes y manejadores de archivos. Creo que C # hace la distinción entre un destructor que se llama cuando el recuento de referencia se pone a cero y cuando decide reciclar la memoria. La gente no está tan preocupada por el reciclaje de la memoria, pero desea saberlo tan pronto como ya no esté referenciado. Es una pena que Python tuviera un potencial real como idioma. Pero el alcance poco convencional y los destructores no confiables (o al menos los que dependen de la implementación) significan que a uno se le niega el poder que obtiene con C ++ y Perl.

Es interesante el comentario sobre el uso de la nueva memoria, si está disponible, en lugar de reciclarla en GC. ¿No es solo una forma elegante de decir que pierde memoria :-)


tl; dr RAII no es posible, se confunde con el alcance en general y cuando se pierden esos ámbitos adicionales, probablemente esté escribiendo un código incorrecto.

Tal vez no entiendo su (s) pregunta (s), o no obtiene algunas cosas esenciales sobre Python ... Primero que nada, la destrucción de objetos determinista relacionada con el alcance es imposible en un lenguaje de recolección de basura. Las variables en Python son meramente referencias. No querrías que un pedazo de memoria de Malloc se free tan pronto como un puntero que lo apunte se salga del alcance, ¿verdad? Excepción práctica en algunas circunstancias si utiliza el recuento de ref, pero ningún lenguaje es lo suficientemente loco como para establecer la implementación exacta en piedra.

E incluso si tiene recuento de referencias, como en CPython, es un detalle de implementación. Por lo general, incluso en Python, que tiene varias implementaciones que no utilizan el recuento de ref, debe codificar como si cada objeto estuviera colgado hasta que la memoria se agotara.

En cuanto a los nombres existentes para el resto de una invocación de función: puede eliminar un nombre del ámbito actual o global a través de la declaración del . Sin embargo, esto no tiene nada que ver con la gestión manual de la memoria. Simplemente elimina la referencia. Eso puede o no puede suceder para activar el objeto referenciado para que sea GC''d y no es el punto del ejercicio.

  • Si su código es lo suficientemente largo para que esto cause conflictos de nombres, debe escribir funciones más pequeñas. Y use nombres más descriptivos, menos propensos a chocar. Lo mismo para los bucles anidados que sobrescriben la variable de iteración del bucle de salida: todavía tengo que encontrar este problema, ¿quizás sus nombres no son lo suficientemente descriptivos o debería factorizar estos bucles?

Usted tiene razón, no tiene nada que ver con el alcance, solo con la limpieza determinista (por lo que se superpone con RAII en los extremos, pero no en los medios).

Tal vez estoy tratando de quitar los beneficios de los aspectos dinámicos del lenguaje. ¿Es egoísta querer a veces aplicar un alcance?

No. El alcance léxico decente es un mérito independiente de la dinámica / estática. Es cierto que Python (2 - 3 bastante arreglado esto) tiene debilidades en este sentido, aunque son más en el ámbito de los cierres.

Pero para explicar "por qué": Python debe ser conservador con el lugar donde comienza un nuevo ámbito, ya que sin una declaración que indique lo contrario, la asignación de un nombre lo convierte en un ámbito local al ámbito más interno / actual. Así que, por ejemplo, si un bucle for tuviera su propio alcance, no podría modificar fácilmente las variables fuera del bucle.

¿Soy perezoso por querer que el compilador / intérprete detecte mis errores de reutilización de variables negligentes? Bueno, sí, por supuesto que soy perezoso, pero ¿soy perezoso de mala manera?

Una vez más, me imagino que la reutilización accidental de un nombre (de una manera que introduce errores o escollos) es rara y pequeña de todos modos.

Edit: Para indicar esto de nuevo tan claramente como sea posible:

  • No puede haber limpieza basada en pila en un lenguaje usando GC. Simplemente no es posible, por definición: una variable es potencialmente una de las muchas referencias a objetos en el montón que ni sabe ni se preocupa cuando las variables están fuera del alcance, y toda la gestión de la memoria está en manos del GC, que se ejecuta cuando le gusta, no cuando se hace estallar un marco de pila. La limpieza de recursos se resuelve de manera diferente, ver más abajo.
  • La limpieza determinista se realiza a través de la declaración. Sí, no introduce un nuevo alcance (ver más abajo), porque no es para eso. No importa que elimine el nombre al que está vinculado el objeto administrado, no se eliminó; sin embargo, la limpieza se llevó a cabo, lo que queda es un objeto "no me toques, soy inutilizable" (por ejemplo, un flujo de archivos cerrado).
  • Python tiene un alcance por función, clase y módulo. Período. Así es como funciona el lenguaje, te guste o no. Si desea / "necesita" un alcance más preciso, divida el código en funciones más detalladas. Es posible que desee un alcance más detallado, pero no lo hay, y por las razones señaladas anteriormente en esta respuesta (tres párrafos sobre la "Edición:"), existen razones para esto. Te guste o no, pero así es como funciona el lenguaje.