Devo passare i nomi dei file da aprire o aprire i file?


53

Supponiamo che io abbia una funzione che fa le cose con un file di testo - ad esempio, legge da esso e rimuove la parola "a". Potrei passargli un nome di file e gestire l'apertura / chiusura nella funzione, oppure potrei passargli il file aperto e aspettarmi che chiunque lo chiami si occupi di chiuderlo.

Il primo modo sembra un modo migliore per garantire che nessun file venga lasciato aperto, ma mi impedisce di usare cose come gli oggetti StringIO

Il secondo modo potrebbe essere un po 'pericoloso - nessun modo di sapere se il file verrà chiuso o meno, ma sarei in grado di usare oggetti simili a file

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)

Uno di questi è generalmente preferito? Si prevede generalmente che una funzione si comporterà in uno di questi due modi? O dovrebbe essere ben documentato in modo tale che il programmatore possa usare la funzione nel modo appropriato?

Risposte:


39

Le interfacce convenienti sono belle e talvolta la strada da percorrere. Tuttavia, il più delle volte una buona compostabilità è più importante della convenienza , in quanto un'astrazione componibile ci consente di implementare altre funzionalità (inclusi i wrapper per la convenienza).

Il modo più generale per la tua funzione di utilizzare i file è prendere un handle di file aperto come parametro, in quanto ciò consente di utilizzare anche handle di file che non fanno parte del filesystem (es. Pipe, socket, ...):

def your_function(open_file):
    return do_stuff(open_file)

Se l'ortografia with open(filename, 'r') as f: result = your_function(f)è troppo da chiedere ai tuoi utenti, puoi scegliere una delle seguenti soluzioni:

  • your_functionaccetta un file aperto o un nome file come parametro. Se è un nome file, il file viene aperto e chiuso e le eccezioni vengono propagate. C'è un po 'di un problema con l'ambiguità qui che potrebbe essere aggirato usando argomenti denominati.
  • Offri un semplice wrapper che si occupi dell'apertura del file, ad es

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

    In genere percepisco funzioni come API bloat, ma se forniscono funzionalità di uso comune, la convenienza acquisita è un argomento sufficientemente forte.

  • Avvolgi la with openfunzionalità in un'altra funzione componibile:

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

    usato come with_file(name, your_function)o in casi più complicatiwith_file(name, lambda f: some_function(1, 2, f, named=4))


6
L'unico inconveniente di questo approccio è che a volte è necessario il nome dell'oggetto simile a un file, ad esempio per la segnalazione degli errori: gli utenti finali preferiscono vedere "Errore in foo.cfg (12)" piuttosto che "Errore in <stream @ 0x03fd2bb6> (12)". A your_functionquesto proposito è possibile utilizzare un argomento "stream_name" facoltativo .

22

La vera domanda è di completezza. La tua funzione di elaborazione dei file è l'elaborazione completa del file o è solo un pezzo in una catena di fasi di elaborazione? Se è completo e di per sé, sentiti libero di incapsulare tutto l'accesso ai file all'interno di una funzione.

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

Questa ha la proprietà molto bella di finalizzare la risorsa (chiusura del file) alla fine withdell'istruzione.

Se tuttavia è possibile che sia necessario elaborare un file già aperto, allora la distinzione tra te ver_1e ver_2ha più senso. Per esempio:

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)

Questo tipo di test esplicito del tipo è spesso disapprovato , specialmente in linguaggi come Java, Julia e Go in cui il dispacciamento basato sul tipo o sull'interfaccia è direttamente supportato. In Python, tuttavia, non esiste alcun supporto linguistico per il dispacciamento basato sul tipo. Di tanto in tanto potresti vedere critiche alla prova diretta del tipo in Python, ma in pratica è sia estremamente comune che abbastanza efficace. Permette a una funzione di avere un alto grado di generalità, gestendo qualsiasi tipo di dato sia probabile che arrivi, noto anche come "tipizzazione anatra". Nota il carattere di sottolineatura iniziale su _ver_file; questo è un modo convenzionale di designare una funzione (o metodo) "privata". Sebbene tecnicamente possa essere chiamato direttamente, suggerisce che la funzione non è destinata al consumo esterno diretto.


Aggiornamento 2019: dati i recenti aggiornamenti in Python 3, ad esempio che i percorsi sono ora potenzialmente archiviati come pathlib.Pathoggetti non solo stro bytes(3.4+) e che il tipo di suggerimento è passato dall'esoterico al mainstream (circa 3.6+, sebbene continui a evolversi attivamente), ecco codice aggiornato che tiene conto di questi progressi:

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)

1
La digitazione anatra testerebbe in base a cosa puoi fare con l'oggetto, piuttosto che al suo tipo. Ad esempio, provando a chiamare readqualcosa che potrebbe essere simile a un file o chiamando open(fileobj, 'r')e catturando TypeErrorif fileobjnon è una stringa.
user2357112

Stai discutendo per la tipizzazione di anatre in uso . L'esempio fornisce una digitazione anatra in effetti , ovvero gli utenti ottengono l' veroperazione indipendentemente dal tipo. Potrebbe anche essere possibile implementare verattraverso la digitazione anatra, come dici tu. Ma generare quindi eccezioni di cattura è più lento della semplice ispezione del tipo e l'IMO non produce alcun vantaggio particolare (chiarezza, generalità, ecc.) Nella mia esperienza, la tipizzazione di anatre è fantastica "nel grande", ma neutra al controproducente "nel piccolo ".
Jonathan Eunice,

3
No, quello che stai facendo non è ancora la battitura a papera. Un hasattr(fileobj, 'read')test sarebbe la tipizzazione delle anatre; un isinstance(fileobj, str)test non lo è. Ecco un esempio della differenza: il isinstancetest ha esito negativo con i nomi di file Unicode, poiché u'adsf.txt'non è un str. Hai provato per un tipo troppo specifico. Un test di dattilografia, basato sulla chiamata openo su qualche ipotetica does_this_object_represent_a_filenamefunzione, non avrebbe questo problema.
user2357112

1
Se il codice fosse un codice di produzione piuttosto che un esempio esplicativo, non avrei nemmeno quel problema, perché non userei is_instance(x, str)ma piuttosto qualcosa di simile is_instance(x, string_types), con l' string_typesimpostazione corretta per il corretto funzionamento tra PY2 e PY3. Dato qualcosa che si spezza come una stringa, verreagirebbe correttamente; dato qualcosa che cade come un file, lo stesso. Per un utente di ver, non ci sarebbe alcuna differenza, tranne che l'implementazione dell'ispezione del tipo sarebbe più veloce. Puristi di anatre: sentiti libero di non essere d'accordo.
Jonathan Eunice,

5

Se si passa il nome del file in giro anziché l'handle del file, non vi è alcuna garanzia che il secondo file sia lo stesso file del primo quando viene aperto; questo può portare a errori di correttezza e falle di sicurezza.


1
Vero. Ma questo deve essere controbilanciato con un altro compromesso: se si passa attorno a un handle di file, tutti i lettori devono coordinare i loro accessi al file, perché ognuno probabilmente sposta la "posizione corrente del file".
Jonathan Eunice,

@JonathanEunice: coordinare in che senso? Tutto quello che devono fare è impostare la posizione del file in modo che sia dove vogliono che sia.
Mehrdad,

1
Se ci sono più entità che leggono il file, potrebbero esserci dipendenze. Potrebbe essere necessario iniziare da dove ne era stato interrotto un altro (o in un luogo definito dai dati letti da una lettura precedente). Inoltre, i lettori potrebbero essere in esecuzione in thread diversi, aprendo altre lattine di coordinamento di worm. Gli oggetti file trasferiti diventano stati globali esposti, con tutti i problemi (oltre ai vantaggi) che ne derivano.
Jonathan Eunice,

1
Non sta passando il percorso del file che è la chiave. Sta avendo una funzione (o classe, metodo o altro luogo di controllo) assumersi la responsabilità di "l'elaborazione completa del file". Se gli accessi ai file sono incapsulati da qualche parte , non è necessario passare attraverso uno stato globale mutevole come gli handle di file aperti.
Jonathan Eunice,

1
Bene, possiamo concordare di non essere d'accordo allora. Sto dicendo che c'è un aspetto negativo deciso nei progetti che passano agilmente attorno allo stato globale mutevole. Ci sono anche alcuni vantaggi. Pertanto, un "compromesso". I progetti che passano percorsi di file spesso eseguono I / O in un colpo solo, in modo incapsulato. Lo vedo come un accoppiamento vantaggioso. YMMV.
Jonathan Eunice,

1

Si tratta della proprietà e della responsabilità di chiudere il file. Puoi passare un flusso o un handle di file o qualsiasi cosa che dovrebbe essere chiusa / disposta ad un certo punto a un altro metodo, a patto che tu sia sicuro di chi lo possiede e certo che verrà chiuso dal proprietario quando hai finito . Ciò implica in genere un costrutto try-finally o il modello usa e getta.


-1

Se scegli di passare file aperti puoi fare qualcosa come il seguente MA NON hai accesso al nome file nella funzione che scrive nel file.

Lo farei se volessi avere una classe che era responsabile al 100% delle operazioni su file / stream e altre classi o funzioni che sarebbero state ingenue e che non si prevedeva di aprire o chiudere detti file / flussi.

Ricorda che i gestori di contesto funzionano come avere una clausola finally. Quindi, se viene generata un'eccezione nella funzione di scrittura, il file verrà chiuso in ogni caso.

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)

In che modo è meglio / diverso dal semplice utilizzo with open? In che modo questo risolve la questione dell'utilizzo di nomi di file e oggetti simili a file?
Dannnno,

Questo ti mostra un modo per nascondere il comportamento di apertura / chiusura del file / flusso. Come puoi vedere chiaramente nei commenti, ti dà il modo di aggiungere una logica prima di aprire il flusso / file che è trasparente per lo "scrittore". Lo "scrittore" potrebbe essere un metodo di una classe di un altro pacchetto. In sostanza è un involucro di open. Inoltre, grazie per aver risposto e votato.
Vls,

Quel comportamento è già gestito da with open, giusto? E ciò di cui stai effettivamente sostenendo è una funzione che utilizza solo oggetti simili a file e non ti interessa da dove proviene?
Dannnno,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.