ejemplos - ¿Es seguro combinar ''con'' y ''rendimiento'' en python?
django (2)
Es un lenguaje común en Python usar el administrador de contexto para cerrar archivos automáticamente:
with open(''filename'') as my_file:
# do something with my_file
# my_file gets automatically closed after exiting ''with'' block
Ahora quiero leer contenidos de varios archivos. El consumidor de los datos no sabe o le importa si los datos provienen de archivos o no archivos. No quiere comprobar si los objetos que recibió pueden estar abiertos o no. Sólo quiere obtener algo para leer líneas de. Así que creo un iterador como este:
def select_files():
"""Yields carefully selected and ready-to-read-from files"""
file_names = [.......]
for fname in file_names:
with open(fname) as my_open_file:
yield my_open_file
Este iterador puede ser usado así:
for file_obj in select_files():
for line in file_obj:
# do something useful
(Tenga en cuenta que el mismo código podría usarse para consumir no los archivos abiertos, sino las listas de cadenas, ¡eso es genial!)
La pregunta es: ¿es seguro producir archivos abiertos?
Parece que "¿por qué no?". El consumidor llama al iterador, el iterador abre el archivo y lo entrega al consumidor. El consumidor procesa el archivo y vuelve al iterador para el siguiente. Se reanuda el código del iterador, salimos del bloque ''with'', el objeto my_open_file
se cierra, vamos al siguiente archivo, etc.
Pero, ¿y si el consumidor nunca vuelve al iterador para el siguiente archivo? Se produjo una excepción dentro del consumidor. ¿O el consumidor encontró algo muy emocionante en uno de los archivos y felizmente devolvió los resultados a quien lo haya llamado?
El código del iterador nunca se reanudaría en este caso, nunca llegaríamos al final del bloque ''with'', ¡y el objeto my_open_file
nunca se cerraría!
¿O sería?
¿Es seguro combinar ''con'' y ''rendimiento'' en python?
No creo que debas hacer esto.
Déjame demostrarte haciendo algunos archivos:
>>> for f in ''abc'':
... with open(f, ''w'') as _: pass
Convéncete de que los archivos están ahí:
>>> for f in ''abc'':
... with open(f) as _: pass
Y aquí hay una función que recrea tu código:
def gen_abc():
for f in ''abc'':
with open(f) as file:
yield file
Aquí parece que puedes usar la función:
>>> [f.closed for f in gen_abc()]
[False, False, False]
Pero creemos primero una lista de todos los objetos de archivo:
>>> l = [f for f in gen_abc()]
>>> l
[<_io.TextIOWrapper name=''a'' mode=''r'' encoding=''cp1252''>, <_io.TextIOWrapper name=''b'' mode=''r'' encoding=''cp1252''>, <_io.TextIOWrapper name=''c'' mode=''r'' encoding=''cp1252''>]
Y ahora vemos que todos están cerrados:
>>> c = [f.closed for f in l]
>>> c
[True, True, True]
Esto solo funciona hasta que el generador se cierre. Entonces todos los archivos están cerrados.
Dudo que sea lo que quieres, incluso si estás usando una evaluación perezosa, tu último archivo probablemente se cerrará antes de que termines de usarlo.
Usted trae una crítica que se ha planteado antes 1 . La limpieza en este caso no es determinista, pero ocurrirá con CPython cuando el generador obtenga la basura recolectada. Su kilometraje puede variar para otras implementaciones de python ...
Aquí hay un ejemplo rápido:
from __future__ import print_function
import contextlib
@contextlib.contextmanager
def manager():
"""Easiest way to get a custom context manager..."""
try:
print(''Entered'')
yield
finally:
print(''Closed'')
def gen():
"""Just a generator with a context manager inside.
When the context is entered, we''ll see "Entered" on the console
and when exited, we''ll see "Closed" on the console.
"""
man = manager()
with man:
for i in range(10):
yield i
# Test what happens when we consume a generator.
list(gen())
def fn():
g = gen()
next(g)
# g.close()
# Test what happens when the generator gets garbage collected inside
# a function
print(''Start of Function'')
fn()
print(''End of Function'')
# Test what happens when a generator gets garbage collected outside
# a function. IIRC, this isn''t _guaranteed_ to happen in all cases.
g = gen()
next(g)
# g.close()
print(''EOF'')
Ejecutando este script en CPython, obtengo:
$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
EOF
Closed
Básicamente, lo que vemos es que, para los generadores que están agotados, el administrador de contexto se limpia cuando espera. Para los generadores que no están agotados, la función de limpieza se ejecuta cuando el recolector de basura recoge el generador. Esto sucede cuando el generador queda fuera del alcance (o, IIRC a más gc.collect
en el siguiente ciclo gc.collect
).
Sin embargo, haciendo algunos experimentos rápidos (por ejemplo, ejecutando el código anterior en pypy
), no puedo limpiar todos mis administradores de contexto:
$ pypy --version
Python 2.7.10 (f3ad1e1e1d62, Aug 28 2015, 09:36:42)
[PyPy 2.6.1 with GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)]
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
End of Function
Entered
EOF
Por lo tanto, la afirmación de que __exit__
del administrador de __exit__
se llamará para todas las implementaciones de python no es cierta. Probablemente los errores aquí son atribuibles a la estrategia de recolección de basura de Pypy (que no es el recuento de referencias) y para cuando pypy
decide cosechar los generadores, el proceso ya se está cerrando y, por lo tanto, no se molesta con eso ... En la mayoría de las aplicaciones del mundo real, los generadores probablemente se cosecharán y finalizarán lo suficientemente rápido como para que realmente no importe ...
Proporcionando estrictas garantías
Si desea garantizar que su administrador de contexto se finalice correctamente, debe tener cuidado de close el generador cuando haya terminado con él 2 . g.close()
líneas g.close()
arriba me da una limpieza determinista porque se g.close()
un GeneratorExit
en la declaración de yield
(que está dentro del administrador de contexto) y luego el generador la captura / suprime ...
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
$ python3 ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
FWIW, esto significa que puede limpiar sus generadores usando contextlib.closing
:
from contextlib import closing
with closing(gen_function()) as items:
for item in items:
pass # Do something useful!
1 Más recientemente, algunas discusiones giraron en torno al PEP 533, cuyo objetivo es hacer que la limpieza del iterador sea más determinista.
2 Está perfectamente bien cerrar un generador ya cerrado y / o consumido para que pueda llamarlo sin preocuparse por el estado del generador.