Soll ich Dateinamen zum Öffnen übergeben oder Dateien öffnen?

53

Angenommen, ich habe eine Funktion, die Dinge mit einer Textdatei ausführt - zum Beispiel liest sie aus und entfernt das Wort 'a'. Ich könnte ihm entweder einen Dateinamen übergeben und das Öffnen / Schließen in der Funktion handhaben, oder ich könnte ihm die geöffnete Datei übergeben und erwarten, dass jeder, der sie aufruft, damit umgehen würde, sie zu schließen.

Der erste Weg scheint ein besserer Weg zu sein, um sicherzustellen, dass keine Dateien offen bleiben, hindert mich jedoch daran, Dinge wie StringIO-Objekte zu verwenden

Der zweite Weg könnte ein wenig gefährlich sein - keine Möglichkeit zu wissen, ob die Datei geschlossen wird oder nicht, aber ich könnte dateiähnliche Objekte verwenden

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)

Ist eine davon generell bevorzugt? Wird allgemein erwartet, dass sich eine Funktion auf eine dieser beiden Arten verhält? Oder sollte es nur so gut dokumentiert sein, dass der Programmierer die Funktion entsprechend nutzen kann?

Dannnno
quelle

Antworten:

39

Praktische Benutzeroberflächen sind nett und manchmal der richtige Weg. In den meisten Fällen ist jedoch eine gute Kompositionsfähigkeit wichtiger als Bequemlichkeit , da eine kompositionsfähige Abstraktion es uns ermöglicht, andere Funktionen (einschließlich Convenience-Wrapper) darüber zu implementieren.

Die allgemeinste Möglichkeit für Ihre Funktion, Dateien zu verwenden, besteht darin, ein geöffnetes Dateihandle als Parameter zu verwenden, da dies die Verwendung von Dateihandles ermöglicht, die nicht zum Dateisystem gehören (z. B. Pipes, Sockets usw.):

def your_function(open_file):
    return do_stuff(open_file)

Wenn with open(filename, 'r') as f: result = your_function(f)Ihre Benutzer zu viele Fragen zur Rechtschreibung haben , können Sie eine der folgenden Lösungen wählen:

  • your_functionNimmt eine geöffnete Datei oder einen Dateinamen als Parameter. Wenn es sich um einen Dateinamen handelt, wird die Datei geöffnet und geschlossen, und Ausnahmen werden weitergegeben. Es gibt hier ein kleines Problem mit Mehrdeutigkeiten, das mit benannten Argumenten umgangen werden könnte.
  • Bieten Sie einen einfachen Wrapper an, der das Öffnen der Datei übernimmt, z

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

    Ich nehme solche Funktionen im Allgemeinen als "API bloat" wahr, aber wenn sie häufig verwendete Funktionen bereitstellen, ist der gewonnene Komfort ein hinreichend starkes Argument.

  • Wickeln Sie die with openFunktionalität in eine andere zusammensetzbare Funktion ein:

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

    verwendet als with_file(name, your_function)oder in komplizierteren Fällenwith_file(name, lambda f: some_function(1, 2, f, named=4))

amon
quelle
6
Der einzige Nachteil dieses Ansatzes ist, dass manchmal der Name des dateiähnlichen Objekts benötigt wird, z. B. für die Fehlermeldung: Endbenutzer ziehen es vor, "Fehler in foo.cfg (12)" anstelle von "Fehler in <stream @ 0x03fd2bb6>" anzuzeigen (12) ". your_functionIn diesem Zusammenhang kann ein optionales Argument "stream_name" verwendet werden.
22

Die eigentliche Frage ist die Vollständigkeit. Handelt es sich bei Ihrer Dateiverarbeitungsfunktion um die vollständige Verarbeitung der Datei, oder handelt es sich nur um ein Teil einer Reihe von Verarbeitungsschritten? Wenn es vollständig ist, können Sie den gesamten Dateizugriff in einer Funktion einschließen.

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

Dies hat die sehr schöne Eigenschaft, die Ressource am Ende der withAnweisung zu finalisieren (die Datei zu schließen) .

Wenn es jedoch möglicherweise erforderlich ist, eine bereits geöffnete Datei zu verarbeiten, ist die Unterscheidung von ver_1und ver_2sinnvoller. Zum Beispiel:

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)

Diese Art der expliziten Typprüfung wird häufig verpönt , insbesondere in Sprachen wie Java, Julia und Go, in denen typ- oder schnittstellenbasiertes Dispatching direkt unterstützt wird. In Python gibt es jedoch keine Sprachunterstützung für typbasiertes Dispatching. Es kann gelegentlich vorkommen, dass Sie in Python Kritik an direkten Typprüfungen üben, in der Praxis ist dies jedoch äußerst verbreitet und recht effektiv. Es ermöglicht einer Funktion ein hohes Maß an Allgemeingültigkeit, wobei alle Datentypen behandelt werden, die wahrscheinlich ihren Weg finden, auch bekannt als "Duck Typing". Beachten Sie den führenden Unterstrich an _ver_file; Dies ist eine herkömmliche Art, eine "private" Funktion (oder Methode) zu bezeichnen. Sie kann zwar technisch direkt aufgerufen werden, weist jedoch darauf hin, dass die Funktion nicht für den direkten externen Verbrauch vorgesehen ist.


Update 2019: Angesichts der jüngsten Aktualisierungen in Python 3, zum Beispiel, dass Pfade jetzt möglicherweise pathlib.Pathnicht nur als Objekte stroder bytes(3.4+) gespeichert werden und dieser Hinweis vom Esoterischen zum Mainstream übergegangen ist (ca. 3.6+, obwohl er sich immer noch aktiv weiterentwickelt), hier aktualisierter Code, der diese Fortschritte berücksichtigt:

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
quelle
1
Duck Typing würde testen, was Sie mit dem Objekt tun können, und nicht, was für ein Typ es ist. Zum Beispiel, wenn Sie versuchen, readetwas aufzurufen , das einer Datei ähnelt , oder wenn Sie open(fileobj, 'r')das TypeErrorif aufrufen und abfangen, das fileobjkeine Zeichenfolge ist.
user2357112
Sie sprechen sich dafür aus, dass Enten im Einsatz tippen . In diesem Beispiel wird die Eingabe von Enten ausgeführt , dh Benutzer erhalten die verOperation unabhängig vom Typ. Es könnte auch möglich sein ver, wie Sie sagen, durch Duck Typing zu implementieren . Das Erzeugen von Ausnahmen ist jedoch langsamer als das einfache Prüfen von Typen, und IMO bietet keinen besonderen Vorteil (Klarheit, Allgemeingültigkeit usw.). Meiner Erfahrung nach ist das Tippen von Enten "im Großen" großartig, aber im Kleinen neutral bis kontraproduktiv . "
Jonathan Eunice
3
Nein, was Sie immer noch tun, ist nicht das Tippen von Enten. Ein hasattr(fileobj, 'read')Test wäre Entenschreiben; Ein isinstance(fileobj, str)Test ist das nicht. Hier ist ein Beispiel für den Unterschied: Der isinstanceTest schlägt mit Unicode-Dateinamen fehl, da dies u'adsf.txt'nicht der Fall ist str. Sie haben auf einen zu spezifischen Typ getestet. Ein Enten-Typisierungstest, ob basierend auf Aufrufen openoder einer hypothetischen does_this_object_represent_a_filenameFunktion, hätte dieses Problem nicht.
user2357112
1
Wenn der Code eher ein Produktionscode als ein erläuterndes Beispiel wäre, hätte ich dieses Problem auch nicht, da ich ihn nicht verwenden würde, is_instance(x, str)sondern eher so etwas wie is_instance(x, string_types), string_typeswenn er für den ordnungsgemäßen Betrieb zwischen PY2 und PY3 richtig eingestellt wäre. Wenn etwas gegeben wird, das wie eine Schnur quakt, verwürde es richtig reagieren. etwas gegeben, das wie eine Datei quakt, das gleiche. Für einen Benutzer von verwürde es keinen Unterschied geben - außer dass die Implementierung der Typprüfung schneller laufen würde. Entenpuristen: Zögern Sie nicht.
Jonathan Eunice
5

Wenn Sie den Dateinamen anstelle des Datei-Handles weitergeben, kann nicht garantiert werden, dass die zweite Datei dieselbe Datei ist wie die erste, wenn sie geöffnet wird. Dies kann zu Korrektheitsproblemen und Sicherheitslücken führen.

Mehrdad
quelle
1
Wahr. Dies muss jedoch durch einen anderen Kompromiss ausgeglichen werden: Wenn Sie einen Datei-Handle umgehen, müssen alle Leser ihre Zugriffe auf die Datei koordinieren, da jeder wahrscheinlich die "aktuelle Dateiposition" verschieben wird.
Jonathan Eunice
@ JonathanEunice: Koordinieren in welchem ​​Sinne? Sie müssen lediglich die Dateiposition so einstellen, dass sie dort ist, wo sie möchten.
Mehrdad
1
Wenn die Datei von mehreren Entitäten gelesen wird, können Abhängigkeiten bestehen. Man muss möglicherweise dort beginnen, wo ein anderer aufgehört hat (oder an einer Stelle, die durch Daten definiert ist, die von einem vorhergehenden Lesevorgang gelesen wurden). Leser können auch in anderen Threads ausgeführt werden, wodurch sich andere Koordinationsdosen von Würmern öffnen. Übergebene Dateiobjekte werden mit allen damit verbundenen Problemen (und Vorteilen) als globaler Status angezeigt.
Jonathan Eunice
1
Es geht nicht um den Dateipfad, der der Schlüssel ist. Eine Funktion (oder Klasse, Methode oder anderer Kontrollbereich) übernimmt die Verantwortung für die "vollständige Verarbeitung der Datei". Wenn Dateizugriffe irgendwo gekapselt sind , müssen Sie keinen veränderlichen globalen Status wie offene Dateihandles weitergeben.
Jonathan Eunice
1
Nun, wir können uns darauf einigen, nicht zuzustimmen. Ich sage, es gibt einen entschiedenen Nachteil bei Designs, die sich auf glatte Weise um einen veränderlichen globalen Staat drehen. Es gibt auch einige Vorteile. Also ein "Kompromiss". Entwürfe, die Dateipfade durchlaufen, führen E / A-Vorgänge häufig gekapselt auf einen Schlag aus. Ich sehe das als vorteilhafte Kopplung. YMMV.
Jonathan Eunice
1

Hier geht es um das Eigentum und die Verantwortung, die Datei zu schließen. Sie können auf einem Stream oder Datei - Handle übergeben oder was auch immer Dingen , das geschlossen werden soll / an einem gewissen Punkt zu einem anderen Verfahren angeordnet ist , solange Sie sicherstellen , dass es klar ist , wer es besitzt und sicher , es wird vom Eigentümer geschlossen werden , wenn Sie fertig sind , . Dies beinhaltet typischerweise ein Endversuchskonstrukt oder das Wegwerfmuster.

Martin Maat
quelle
-1

Wenn Sie geöffnete Dateien übergeben möchten, können Sie etwas wie das folgende tun, ABER Sie haben keinen Zugriff auf den Dateinamen in der Funktion, die in die Datei schreibt.

Ich würde dies tun, wenn ich eine Klasse haben möchte, die zu 100% für Datei- / Stream-Vorgänge verantwortlich ist, und andere Klassen oder Funktionen, die naiv wären und die Dateien / Streams nicht öffnen oder schließen sollen.

Denken Sie daran, dass Kontextmanager wie eine finally-Klausel funktionieren. Wenn also eine Ausnahme in der Writer-Funktion ausgelöst wird, wird die Datei geschlossen, egal was passiert.

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
quelle
Wie ist das besser / anders als nur mit with open? Wie wird die Frage der Verwendung von Dateinamen im Vergleich zu dateiähnlichen Objekten beantwortet?
Dannnno
Dies zeigt Ihnen, wie Sie das Öffnen / Schließen von Dateien / Streams verbergen können. Wie Sie in den Kommentaren deutlich sehen können, können Sie Logik hinzufügen, bevor Sie den Stream / die Datei öffnen, der / die für den "Writer" transparent ist. Der "Schreiber" könnte eine Methode einer Klasse eines anderen Pakets sein. Im Wesentlichen ist es ein Wrapper of Open. Vielen Dank für Ihre Antwort und Ihr Voting.
Vls
Dieses Verhalten wird aber schon von behandelt with open, oder? Und was Sie effektiv befürworten, ist eine Funktion, die nur dateiähnliche Objekte verwendet und sich nicht darum kümmert, woher sie stammt?
Dannnno