¿Debo pasar los nombres de archivo para abrirlos o abrir archivos?

53

Supongamos que tengo una función que hace cosas con un archivo de texto; por ejemplo, lee y elimina la palabra 'a'. Podría pasarle un nombre de archivo y manejar la apertura / cierre en la función, o podría pasarle el archivo abierto y esperar que quien lo llame se ocupe de cerrarlo.

La primera forma parece una mejor manera de garantizar que no se dejen archivos abiertos, pero me impide usar cosas como objetos StringIO

La segunda forma podría ser un poco peligrosa: no hay forma de saber si el archivo se cerrará o no, pero podría usar objetos similares a un archivo

def ver_1(filename):
    with open(filename, 'r') as f:
        return do_stuff(f)

def ver_2(open_file):
    return do_stuff(open_file)

print ver_1('my_file.txt')

with open('my_file.txt', 'r') as f:
    print ver_2(f)

¿Es uno de estos generalmente preferido? ¿Generalmente se espera que una función se comporte de una de estas dos maneras? ¿O debería estar bien documentado para que el programador pueda usar la función según corresponda?

Dannnno
fuente

Respuestas:

39

Las interfaces convenientes son agradables y, a veces, el camino a seguir. Sin embargo, la mayoría de las veces la buena capacidad de compilación es más importante que la conveniencia , ya que una abstracción componible nos permite implementar otra funcionalidad (incluidos los envoltorios de conveniencia).

La forma más general para que su función use archivos es tomar un identificador de archivo abierto como parámetro, ya que esto también le permite usar identificadores de archivo que no son parte del sistema de archivos (por ejemplo, tuberías, enchufes, ...):

def your_function(open_file):
    return do_stuff(open_file)

Si deletrear with open(filename, 'r') as f: result = your_function(f)es demasiado pedir a sus usuarios, puede elegir una de las siguientes soluciones:

  • your_functiontoma un archivo abierto o un nombre de archivo como parámetro. Si es un nombre de archivo, el archivo se abre y se cierra, y se propagan excepciones. Aquí hay un pequeño problema con la ambigüedad que podría solucionarse utilizando argumentos con nombre.
  • Ofrezca un contenedor simple que se encargue de abrir el archivo, p. Ej.

    def your_function_filename(file):
        with open(file, 'r') as f:
            return your_function(f)
    

    En general, percibo funciones como la hinchazón de API, pero si proporcionan una funcionalidad de uso común, la conveniencia obtenida es un argumento suficientemente fuerte.

  • Envuelva la with openfuncionalidad en otra función composable:

    def with_file(filename, callback):
        with open(filename, 'r') as f:
            return callback(f)
    

    utilizado como with_file(name, your_function)o en casos más complicadoswith_file(name, lambda f: some_function(1, 2, f, named=4))

amon
fuente
66
El único inconveniente de este enfoque es que a veces se necesita el nombre del objeto similar a un archivo, por ejemplo, para informes de errores: los usuarios finales prefieren ver "Error en foo.cfg (12)" en lugar de "Error en <stream @ 0x03fd2bb6> (12) ". Se your_functionpuede usar un argumento opcional "stream_name" para este respecto.
22

La verdadera pregunta es de integridad. ¿Es su función de procesamiento de archivos el procesamiento completo del archivo, o es solo una pieza en una cadena de pasos de procesamiento? Si está completo por sí solo, no dude en encapsular todo el acceso a archivos dentro de una función.

def ver(filepath):
    with open(filepath, "r") as f:
        # do processing steps on f
        return result

Esto tiene la muy buena propiedad de finalizar el recurso (cerrar el archivo) al final de la withdeclaración.

Sin embargo, si existe la necesidad de procesar un archivo ya abierto, entonces la distinción de su ver_1y ver_2tiene más sentido. Por ejemplo:

def _ver_file(f):
    # do processing steps on f
    return result

def ver(fileobj):
    if isinstance(fileobj, str):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

Este tipo de prueba de tipo explícito a menudo está mal visto , especialmente en lenguajes como Java, Julia y Go, donde el despacho basado en tipo o interfaz es directamente compatible. En Python, sin embargo, no hay soporte de idioma para el despacho basado en tipos. Es posible que de vez en cuando vea críticas de las pruebas de tipo directas en Python, pero en la práctica es extremadamente común y bastante efectivo. Permite que una función tenga un alto grado de generalidad, manejando cualquier tipo de datos que puedan aparecer, también conocido como "escritura de pato". Tenga en cuenta el guión bajo principal en _ver_file; esa es una forma convencional de designar una función (o método) "privada". Si bien técnicamente se puede llamar directamente, sugiere que la función no está destinada al consumo externo directo.


Actualización de 2019: Dadas las actualizaciones recientes en Python 3, por ejemplo, que las rutas ahora se almacenan potencialmente como pathlib.Pathobjetos no solo stro bytes(3.4+), y ese tipo de sugerencia ha pasado de esotérico a mainstream (circa 3.6+, aunque todavía evoluciona activamente), aquí está Código actualizado que tiene en cuenta estos avances:

from pathlib import Path
from typing import IO, Any, AnyStr, Union

Pathish = Union[AnyStr, Path]  # in lieu of yet-unimplemented PEP 519
FileSpec = Union[IO, Pathish]

def _ver_file(f: IO) -> Any:
    "Process file f"
    ...
    return result

def ver(fileobj: FileSpec) -> Any:
    "Process file (or file path) f"
    if isinstance(fileobj, (str, bytes, Path)):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)
Jonathan Eunice
fuente
1
La escritura de pato probaría en función de lo que puede hacer con el objeto, en lugar de cuál es su tipo. Por ejemplo, tratar de invocar readalgo que podría ser similar a un archivo, o llamar open(fileobj, 'r')y capturar el TypeErrorif fileobjno es una cadena.
user2357112
Estás discutiendo por el tipeo de patos en uso . El ejemplo proporciona la escritura de pato en efecto , es decir, los usuarios obtienen la veroperación independientemente del tipo. También puede ser posible implementar a vertravés de la escritura de pato, como usted dice. Pero generar excepciones para capturar es más lento que la simple inspección de tipo, y la OMI no produce ningún beneficio particular (claridad, generalidad, etc.) En mi experiencia, la tipificación de patos es impresionante "en general", pero neutral a contraproducente "en pequeño ".
Jonathan Eunice
3
No, lo que estás haciendo todavía no es escribir pato. Una hasattr(fileobj, 'read')prueba sería escribir pato; una isinstance(fileobj, str)prueba no lo es. Aquí hay un ejemplo de la diferencia: la isinstanceprueba falla con nombres de archivo unicode, ya u'adsf.txt'que no es un str. Has probado un tipo demasiado específico. Una prueba de tipeo de pato, ya sea basada en llamadas openo alguna does_this_object_represent_a_filenamefunción hipotética , no tendría ese problema.
user2357112
1
Si el código fuera un código de producción en lugar de un ejemplo explicativo, tampoco tendría ese problema, porque no lo usaría, is_instance(x, str)sino más bien algo así is_instance(x, string_types), con string_typesun ajuste adecuado para un funcionamiento adecuado en PY2 y PY3. Dado algo que grazna como una cuerda, verreaccionaría adecuadamente; dado algo que grazna como un archivo, lo mismo. Para un usuario de ver, no habría diferencia, excepto que la implementación de inspección de tipo se ejecutaría más rápido. Pura puristas: siéntase libre de estar en desacuerdo.
Jonathan Eunice
5

Si pasa el nombre del archivo en lugar del identificador del archivo, no hay garantía de que el segundo archivo sea el mismo que el primero cuando se abre; Esto puede conducir a errores de corrección y agujeros de seguridad.

Mehrdad
fuente
1
Cierto. Pero eso debe contrarrestarse con otra compensación: si pasa un identificador de archivo, todos los lectores deben coordinar sus accesos al archivo, ya que es probable que cada uno mueva la "posición actual del archivo".
Jonathan Eunice
@ JonathanEunice: ¿Coordinar en qué sentido? Todo lo que necesitan hacer es establecer la posición del archivo para que esté donde quieran.
Mehrdad
1
Si hay varias entidades que leen el archivo, puede haber dependencias. Es posible que deba comenzar donde lo dejó otro (o en un lugar definido por los datos leídos por una lectura anterior). Además, los lectores pueden estar funcionando en diferentes hilos, abriendo otras latas de coordinación de gusanos. Los objetos de archivo pasados ​​se convierten en un estado global expuesto, con todos los problemas (así como los beneficios) que conlleva.
Jonathan Eunice
1
La clave no es pasar por la ruta del archivo. Es tener una función (o clase, método u otro lugar de control) asumir la responsabilidad del "procesamiento completo del archivo". Si los accesos a archivos están encapsulados en algún lugar , entonces no necesita pasar por un estado global mutable como los identificadores de archivos abiertos.
Jonathan Eunice
1
Bueno, podemos estar de acuerdo en no estar de acuerdo entonces. Estoy diciendo que hay una desventaja decidida en los diseños que pasan fácilmente por un estado global mutable. También hay algunas ventajas. Por lo tanto, una "compensación". Los diseños que pasan rutas de archivos a menudo realizan E / S de una sola vez, de forma encapsulada. Veo eso como un acoplamiento ventajoso. YMMV.
Jonathan Eunice
1

Se trata de la propiedad y la responsabilidad de cerrar el archivo. Puede pasar en una referencia de flujo o un archivo o lo que sea cosita que debe ser cerrada / dispuestos en un cierto punto a otro método, siempre y cuando se asegure de que quede claro quién es el dueño y seguro que va a ser cerrado por el propietario cuando haya terminado . Esto generalmente implica una construcción try-finally o el patrón desechable.

Martin Maat
fuente
-1

Si elige pasar archivos abiertos, puede hacer algo como lo siguiente PERO no tiene acceso al nombre de archivo en la función que escribe en el archivo.

Haría esto si quisiera tener una clase que fuera 100% responsable de las operaciones de archivo / secuencia y otras clases o funciones que serían ingenuas y no se espera que abran o cierren dichos archivos / secuencias.

Recuerde que los gestores de contexto funcionan como si tuviera una cláusula finalmente. Entonces, si se lanza una excepción en la función de escritor, el archivo se cerrará sin importar qué.

import contextlib

class FileOpener:

    def __init__(self, path_to_file):
        self.path_to_file = path_to_file

    @contextlib.contextmanager
    def open_write(self):
        # ...
        # Here you can add code to create the directory that will accept the file.
        # ...
        # And you can add code that will check that the file does not exist 
        # already and maybe raise FileExistsError
        # ...
        try:            
            with open(self.path_to_file, "w") as file:
                print(f"open_write: has opened the file with id:{id(file)}")            
                yield file                
        except IOError:
            raise
        finally:
            # The try/catch/finally is not mandatory (except if you want to manage Exceptions in an other way, as file objects have predefined cleanup actions 
            # and when used with a 'with' ie. a context manager (not the decorator in this example) 
            # are closed even if an error occurs. Finally here is just used to demonstrate that the 
            # file was really closed.
            print(f"open_write: has closed the file with id:{id(file)} - {file.closed}")        


def writer(file_open, data, raise_exc):
    with file_open() as file:
        print("writer: started writing data.")
        file.write(data)
        if raise_exc:
            raise IOError("I am a broken data cable in your server!")
        print("writer: wrote data.")
    print("writer: finished.")

if __name__ == "__main__":
    fo = FileOpener('./my_test_file.txt')    
    data = "Hello!"  
    raise_exc = False  # change me to True and see that the file is closed even if an Exception is raised.
    writer(fo.open_write, data, raise_exc)
Vls
fuente
¿Cómo es esto mejor / diferente que simplemente usar with open? ¿Cómo aborda esto la cuestión del uso de nombres de archivo frente a objetos similares a archivos?
Dannnno
Esto le muestra una forma de ocultar el comportamiento de abrir / cerrar archivo / secuencia. Como puede ver claramente en los comentarios, le brinda la forma de agregar lógica antes de abrir la secuencia / archivo que es transparente para el "escritor". El "escritor" podría ser un método de una clase de otro paquete. En esencia es una envoltura de abierto. Además, gracias por responder y votar.
Vls
Sin with openembargo, ese comportamiento ya se maneja , ¿verdad? ¿Y lo que estás defendiendo efectivamente es una función que solo usa objetos similares a archivos y no le importa de dónde provienen?
Dannnno