que - Manera de Pythonic para componer gestores de contexto para objetos propiedad de una clase
que es label en python (5)
Bueno, si desea procesar sin duda los manejadores de archivos, la solución más simple es pasar los manejadores de archivos directamente a su clase en lugar de a los nombres de archivo.
with open(f1, ''r'') as f1, open(f2, ''w'') as f2:
with MyClass(f1, f2) as my_obj:
...
Si no necesita la funcionalidad __exit__
personalizada, puede incluso omitirla.
Si realmente quiere pasar los nombres de archivo a __init__
, su problema podría resolverse así:
class MyClass:
input, output = None, None
def __init__(self, input, output):
try:
self.input = open(input, ''r'')
self.output = open(output, ''w'')
except BaseException as exc:
self.__exit___(type(exc), exc, exc.__traceback__)
raise
def __enter__(self):
return self
def __exit__(self, *args):
self.input and self.input.close()
self.output and self.output.close()
# My custom __exit__ code
Entonces, realmente depende de tu tarea, Python tiene muchas opciones para trabajar. Al final del día, la forma pitónica es mantener su API simple.
Es típico requerir para algunas tareas múltiples objetos que tienen recursos para ser liberados explícitamente, por ejemplo, dos archivos; esto se hace fácilmente cuando la tarea es local para una función que utiliza bloques anidados, o, mejor aún, un bloque único con varias cláusulas with_item
:
with open(''in.txt'', ''r'') as i, open(''out.txt'', ''w'') as o:
# do stuff
OTOH, todavía me cuesta entender cómo se supone que funciona esto cuando dichos objetos no son solo locales para el ámbito de una función, sino que son propiedad de una instancia de clase; en otras palabras, cómo se componen los administradores de contexto.
Idealmente me gustaría hacer algo como:
class Foo:
def __init__(self, in_file_name, out_file_name):
self.i = WITH(open(in_file_name, ''r''))
self.o = WITH(open(out_file_name, ''w''))
y hacer que Foo
se convierta en un administrador de contexto que maneje i
y o
, de manera que cuando lo haga
with Foo(''in.txt'', ''out.txt'') as f:
# do stuff
self.i
y self.o
son atendidos automáticamente como usted esperaría.
Jugué sobre escribir cosas tales como:
class Foo:
def __init__(self, in_file_name, out_file_name):
self.i = open(in_file_name, ''r'').__enter__()
self.o = open(out_file_name, ''w'').__enter__()
def __enter__(self):
return self
def __exit__(self, *exc):
self.i.__exit__(*exc)
self.o.__exit__(*exc)
pero es a la vez detallado e inseguro contra las excepciones que ocurren en el constructor. Después de buscar por un tiempo, encontré esta publicación de blog de 2015 , que usa contextlib.ExitStack
para obtener algo muy similar a lo que busco:
class Foo(contextlib.ExitStack):
def __init__(self, in_file_name, out_file_name):
super().__init__()
self.in_file_name = in_file_name
self.out_file_name = out_file_name
def __enter__(self):
super().__enter__()
self.i = self.enter_context(open(self.in_file_name, ''r'')
self.o = self.enter_context(open(self.out_file_name, ''w'')
return self
Esto es bastante satisfactorio, pero estoy perplejo por el hecho de que:
- No encuentro nada sobre este uso en la documentación, por lo que no parece ser la forma "oficial" de abordar este problema;
- en general, me resulta extremadamente difícil encontrar información sobre este problema, lo que me hace pensar que estoy tratando de aplicar una solución antipónica al problema.
Algún contexto adicional : trabajo principalmente en C ++, donde no hay distinción entre el caso de ámbito de bloque y el caso de objeto de este problema, ya que este tipo de limpieza se implementa dentro del destructor (piense __del__
, pero se invoca de manera determinista), y el destructor (incluso si no está definido explícitamente) invoca automáticamente los destructores de los subobjetos. Por lo tanto:
{
std::ifstream i("in.txt");
std::ofstream o("out.txt");
// do stuff
}
y
struct Foo {
std::ifstream i;
std::ofstream o;
Foo(const char *in_file_name, const char *out_file_name)
: i(in_file_name), o(out_file_name) {}
}
{
Foo f("in.txt", "out.txt");
}
Haz toda la limpieza automáticamente como quieras.
Estoy buscando un comportamiento similar en Python, pero nuevamente, me temo que solo estoy tratando de aplicar un patrón proveniente de C ++, y que el problema subyacente tiene una solución radicalmente diferente en la que no puedo pensar.
Entonces, para resumir: ¿cuál es la solución Pythonic para el problema de tener un objeto que posee objetos que requieren limpieza y convertirse en un administrador de contexto en sí mismo, llamando correctamente el __enter__
/ __exit__
de sus hijos?
Como mencionó @cpburnz, su último ejemplo es el mejor, pero contiene un error si falla la segunda apertura. Evitar este error se describe en la documentación de la biblioteca estándar. Podemos adaptar fácilmente los fragmentos de código de la documentación de ExitStack y el ejemplo de ResourceManager
de 29.6.2.4 Limpiando en una implementación __enter__
para obtener una clase MultiResourceManager
:
from contextlib import contextmanager, ExitStack
class MultiResourceManager(ExitStack):
def __init__(self, resources, acquire_resource, release_resource,
check_resource_ok=None):
super().__init__()
self.acquire_resource = acquire_resource
self.release_resource = release_resource
if check_resource_ok is None:
def check_resource_ok(resource):
return True
self.check_resource_ok = check_resource_ok
self.resources = resources
self.wrappers = []
@contextmanager
def _cleanup_on_error(self):
with ExitStack() as stack:
stack.push(self)
yield
# The validation check passed and didn''t raise an exception
# Accordingly, we want to keep the resource, and pass it
# back to our caller
stack.pop_all()
def enter_context(self, resource):
wrapped = super().enter_context(self.acquire_resource(resource))
if not self.check_resource_ok(wrapped):
msg = "Failed validation for {!r}"
raise RuntimeError(msg.format(resource))
return wrapped
def __enter__(self):
with self._cleanup_on_error():
self.wrappers = [self.enter_context(r) for r in self.resources]
return self.wrappers
# NB: ExitStack.__exit__ is already correct
Ahora tu clase de Foo () es trivial:
import io
class Foo(MultiResourceManager):
def __init__(self, *paths):
super().__init__(paths, io.FileIO, io.FileIO.close)
Esto es bueno porque no necesitamos ningún bloque try-except - ¡probablemente solo estés usando ContextManagers para deshacerte de ellos en primer lugar!
Luego, puede usarlo como quisiera (tenga en cuenta que MultiResourceManager.__enter__
devuelve una lista de los objetos proporcionados por el objeto adquirido_resource ()):
if __name__ == ''__main__'':
open(''/tmp/a'', ''w'').close()
open(''/tmp/b'', ''w'').close()
with Foo(''/tmp/a'', ''/tmp/b'') as (f1, f2):
print(''opened {0} and {1}''.format(f1.name, f2.name))
Podemos reemplazar io.FileIO
con debug_file
como en el siguiente fragmento de debug_file
para verlo en acción:
class debug_file(io.FileIO):
def __enter__(self):
print(''{0}: enter''.format(self.name))
return super().__enter__()
def __exit__(self, *exc_info):
print(''{0}: exit''.format(self.name))
return super().__exit__(*exc_info)
Entonces vemos:
/tmp/a: enter
/tmp/b: enter
opened /tmp/a and /tmp/b
/tmp/b: exit
/tmp/a: exit
Si añadimos import os; os.unlink(''/tmp/b'')
import os; os.unlink(''/tmp/b'')
justo antes del bucle que veríamos:
/tmp/a: enter
/tmp/a: exit
Traceback (most recent call last):
File "t.py", line 58, in <module>
with Foo(''/tmp/a'', ''/tmp/b'') as (f1, f2):
File "t.py", line 46, in __enter__
self.wrappers = [self.enter_context(r) for r in self.resources]
File "t.py", line 46, in <listcomp>
self.wrappers = [self.enter_context(r) for r in self.resources]
File "t.py", line 38, in enter_context
wrapped = super().enter_context(self.acquire_resource(resource))
FileNotFoundError: [Errno 2] No such file or directory: ''/tmp/b''
Puedes ver que / tmp / a está cerrado correctamente.
Creo que contextlib.ExitStack es Pythonic y canonical y es la solución adecuada para este problema. El resto de esta respuesta intenta mostrar los enlaces que solía llegar a esta conclusión y mi proceso de pensamiento:
Solicitud de mejora de Python original
https://bugs.python.org/issue13585
La idea + implementación original se propuso como una mejora de la biblioteca estándar de Python con código de ejemplo y razonamiento. Fue discutido en detalle por desarrolladores clave como Raymond Hettinger y Eric Snow. La discusión sobre este tema muestra claramente el crecimiento de la idea original en algo que es aplicable para la biblioteca estándar y es Pythonic. Intento de resumen del hilo es:
nikratio originalmente propuesto:
Me gustaría proponer que agregue la clase CleanupManager descrita en http://article.gmane.org/gmane.comp.python.ideas/12447 al módulo contextlib. La idea es agregar un administrador de contexto de propósito general para administrar (python o no-python) los recursos que no vienen con su propio administrador de contexto
Que se encontró con las preocupaciones de Rhettinger:
Hasta ahora, ha habido una demanda cero para esto y no he visto un código como el que se usa en la naturaleza. AFAICT, no es demostrablemente mejor que un simple intento / finalmente.
Como respuesta a esto, hubo una larga discusión sobre si era necesario para esto, lo que lleva a publicaciones como estas de ncoghlan:
TestCase.setUp () y TestCase.tearDown () estaban entre los precursores de__enter __ () y exit (). addCleanUp () cumple exactamente el mismo rol aquí, y he visto un montón de comentarios positivos dirigidos a Michael para esa adición a la API de prueba de unidad ... ... Los administradores de contexto personalizados suelen ser una mala idea en estas circunstancias, porque hacen La legibilidad es peor (confiar en que las personas entiendan lo que hace el administrador de contexto). Una solución basada en una biblioteca estándar, por otro lado, ofrece lo mejor de ambos mundos: - el código se vuelve más fácil de escribir correctamente y de auditar la corrección (por todas las razones con declaraciones se agregaron en primer lugar) - el idioma finalmente se convertirá familiar para todos los usuarios de Python ... ... puedo tomar esto en python-dev si lo desea, pero espero convencerlo de que el deseo está ahí ...
Y luego otra vez de ncoghlan un poco más tarde:
Mis descripciones anteriores aquí no son realmente adecuadas, tan pronto como comencé a juntar contextlib2, esta idea de CleanupManager se transformó rápidamente en ContextStack [1], que es una herramienta mucho más poderosa para manipular los administradores de contexto de una manera que no necesariamente corresponde Con alcance léxico en el código fuente.
Ejemplos / recetas / publicaciones de blog de ExitStack Hay varios ejemplos y recetas dentro del código fuente de la biblioteca estándar, que puede ver en la revisión de combinación que agregó esta función: https://hg.python.org/cpython/rev/8ef66c73b1e1
También hay una publicación en el blog del creador original de la edición (Nikolaus Rath / nikratio) que describe de manera convincente por qué ContextStack es un buen patrón y también proporciona algunos ejemplos de uso: https://www.rath.org/on-the-beauty-of-pythons-exitstack.html
Creo que usar un ayudante es mejor:
from contextlib import ExitStack, contextmanager
class Foo:
def __init__(self, i, o):
self.i = i
self.o = o
@contextmanager
def multiopen(i, o):
with ExitStack() as stack:
i = stack.enter_context(open(i))
o = stack.enter_context(open(o))
yield Foo(i, o)
El uso es cercano al nativo open
:
with multiopen(i_name, o_name) as foo:
pass
Su segundo ejemplo es la forma más directa de hacerlo en Python (es decir, la mayoría de Pythonic). Sin embargo, su ejemplo todavía tiene un error. Si se levanta una excepción durante el segundo open()
,
self.i = self.enter_context(open(self.in_file_name, ''r'')
self.o = self.enter_context(open(self.out_file_name, ''w'') # <<< HERE
entonces self.i
no se liberará cuando espere, ya que Foo.__exit__()
no se llamará a menos que Foo.__enter__()
devuelva con éxito. Para solucionar esto, envuelva cada llamada de contexto en un intento, excepto que se llamará Foo.__exit__()
cuando se produzca una excepción.
import contextlib
import sys
class Foo(contextlib.ExitStack):
def __init__(self, in_file_name, out_file_name):
super().__init__()
self.in_file_name = in_file_name
self.out_file_name = out_file_name
def __enter__(self):
super().__enter__()
try:
# Initialize sub-context objects that could raise exceptions here.
self.i = self.enter_context(open(self.in_file_name, ''r''))
self.o = self.enter_context(open(self.out_file_name, ''w''))
except:
if not self.__exit__(*sys.exc_info()):
raise
return self