Czy należy podać nazwy plików, które mają zostać otwarte, czy otworzyć pliki?

53

Załóżmy, że mam funkcję, która robi rzeczy z plikiem tekstowym - na przykład czyta z niego i usuwa słowo „a”. Mógłbym albo przekazać nazwę pliku i obsłużyć otwieranie / zamykanie w funkcji, albo przekazywać otwarty plik i oczekiwać, że ktokolwiek go wywoła, zajmie się jego zamknięciem.

Pierwszy sposób wydaje się lepszym sposobem na zagwarantowanie, że żadne pliki nie zostaną otwarte, ale uniemożliwia mi używanie takich rzeczy jak obiekty StringIO

Drugi sposób może być trochę niebezpieczny - nie ma możliwości dowiedzenia się, czy plik zostanie zamknięty, czy nie, ale mógłbym używać obiektów podobnych do plików

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)

Czy jeden z nich jest ogólnie preferowany? Czy ogólnie oczekuje się, że funkcja będzie zachowywać się na jeden z tych dwóch sposobów? A może powinien być po prostu dobrze udokumentowany, aby programista mógł odpowiednio wykorzystać tę funkcję?

Dannnno
źródło

Odpowiedzi:

39

Wygodne interfejsy są fajne, a czasem do zrobienia. Jednak w większości przypadków dobra kompozycyjność jest ważniejsza niż wygoda , ponieważ abstrakcja kompozycyjna pozwala nam na wdrożenie innych funkcji (w tym wygodnych opakowań).

Najbardziej ogólnym sposobem korzystania z plików przez funkcję jest przyjęcie otwartego uchwytu pliku jako parametru, ponieważ pozwala to również na użycie uchwytów plików, które nie są częścią systemu plików (np. Potoki, gniazda itp.):

def your_function(open_file):
    return do_stuff(open_file)

Jeśli wypowiadanie się with open(filename, 'r') as f: result = your_function(f)jest zbyt wymagające dla użytkowników, możesz wybrać jedno z następujących rozwiązań:

  • your_functionprzyjmuje jako parametr otwarty plik lub nazwę pliku. Jeśli jest to nazwa pliku, plik jest otwierany i zamykany, a wyjątki propagowane. Jest tu pewien problem z dwuznacznością, który można obejść za pomocą nazwanych argumentów.
  • Zaoferuj proste opakowanie, które zajmie się otwarciem pliku, np

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

    Generalnie postrzegam takie funkcje jak nadęty API, ale jeśli zapewniają one powszechnie używane funkcje, uzyskana wygoda jest wystarczająco silnym argumentem.

  • Zawiń with openfunkcjonalność w inną funkcję składaną:

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

    używane jako with_file(name, your_function)lub w bardziej skomplikowanych przypadkachwith_file(name, lambda f: some_function(1, 2, f, named=4))

amon
źródło
6
Jedyną wadą tego podejścia jest to, że czasami potrzebna jest nazwa obiektu podobnego do pliku, np. Do zgłaszania błędów: użytkownicy końcowi wolą widzieć „Błąd w foo.cfg (12)” niż „Błąd w <stream @ 0x03fd2bb6> (12) ”. W your_functiontym względzie można zastosować opcjonalny argument „nazwa_strumienia” .
22

Prawdziwe pytanie dotyczy kompletności. Czy twoja funkcja przetwarzania plików jest kompletnym przetwarzaniem pliku, czy jest to tylko jeden element w szeregu etapów przetwarzania? Jeśli jest on sam w sobie kompletny, możesz obudować cały dostęp do pliku w ramach funkcji.

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

Ma to bardzo przyjemną właściwość finalizowania zasobu (zamykania pliku) na końcu withinstrukcji.

Jeśli jednak istnieje potrzeba przetworzenia już otwartego pliku, wówczas rozróżnienie twojego ver_1i ver_2ma większy sens. Na przykład:

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)

Tego rodzaju jawne testowanie typów jest często lekceważone , szczególnie w takich językach, jak Java, Julia i Go, gdzie bezpośrednie wysyłanie oparte na typie lub interfejsie jest obsługiwane. Jednak w Pythonie nie ma obsługi języka dla wysyłania opartego na typach. Czasami możesz spotkać się z krytyką bezpośrednich testów typu w Pythonie, ale w praktyce jest to zarówno bardzo powszechne, jak i dość skuteczne. Umożliwia dużej ogólności funkcji, obsługując wszystkie typy danych, które mogą się pojawić, czyli „pisanie kaczek”. Zwróć uwagę na wiodący znak podkreślenia na _ver_file; jest to konwencjonalny sposób oznaczania funkcji „prywatnej” (lub metody). Chociaż technicznie można go wywołać bezpośrednio, sugeruje, że funkcja nie jest przeznaczona do bezpośredniego zużycia zewnętrznego.


Aktualizacja 2019: Biorąc pod uwagę najnowsze aktualizacje w Pythonie 3, na przykład ścieżki są teraz potencjalnie przechowywane jako pathlib.Pathobiekty nie tylko strlub bytes(3.4+), a podpowiedzi typu zmieniły się z ezoterycznych na główny nurt (około 3.6+, choć wciąż aktywnie się rozwija), oto: zaktualizowany kod, który uwzględnia te postępy:

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
źródło
1
Pisanie kaczką sprawdzałoby się na podstawie tego, co można zrobić z obiektem, a nie na podstawie jego typu. Na przykład próba wywołania readczegoś, co może być podobne do pliku lub wywołanie open(fileobj, 'r')i przechwycenie TypeErrorif fileobjnie jest łańcuchem.
user2357112,
Argumentujesz za pisaniem kaczek w użyciu . Przykładem zapewnia kaczka wpisując efektu , czyli regulują to użytkownicy otrzymują verniezależne funkcjonowanie typu. verJak można powiedzieć, może być również możliwe wdrożenie za pomocą pisania kaczego. Ale generowanie następnie wychwytywania wyjątków jest wolniejsze niż prosta kontrola typu, a IMO nie przynosi żadnej szczególnej korzyści (jasności, ogólności itp.) Z mojego doświadczenia, pisanie kaczek jest niesamowite „na dużą skalę”, ale neutralne na przeciwne do zamierzonego na małych . ”
Jonathan Eunice,
3
Nie, to, co robisz, nadal nie polega na pisaniu kaczek. hasattr(fileobj, 'read')Badanie byłoby kaczka wpisywanie; isinstance(fileobj, str)test nie jest. Oto przykład różnicy: isinstancetest kończy się niepowodzeniem z nazwami plików Unicode, ponieważ u'adsf.txt'nie jest to str. Testowałeś na zbyt konkretny typ. Test pisania na kaczce, bez względu na to, czy jest wywołany, openczy jakąś hipotetyczną does_this_object_represent_a_filenamefunkcję, nie miałby tego problemu.
user2357112,
1
Gdyby kod był kodem produkcyjnym, a nie objaśniającym, również nie miałbym tego problemu, ponieważ nie użyłbym is_instance(x, str)raczej czegoś podobnego is_instance(x, string_types), z string_typesodpowiednio ustawionym działaniem w PY2 i PY3. Biorąc pod uwagę coś, co trzeszczy jak struna, verzareaguje prawidłowo; biorąc pod uwagę coś, co kwakuje jak plik, to samo. Do użytkownika o ver, nie byłoby żadnej różnicy - poza tym, że realizacja inspekcja typ będzie działać szybciej. Kaczki purystów: nie krępuj się.
Jonathan Eunice,
5

Jeśli podasz nazwę pliku zamiast uchwytu pliku, nie ma gwarancji, że drugi plik jest tym samym plikiem, co pierwszy plik po otwarciu; może to prowadzić do błędów poprawności i dziur bezpieczeństwa.

Mehrdad
źródło
1
Prawdziwe. Ale należy to zrównoważyć innym kompromisem: jeśli ominiesz uchwyt pliku, wszyscy czytelnicy muszą skoordynować dostęp do pliku, ponieważ każdy z nich może przesunąć „bieżącą pozycję pliku”.
Jonathan Eunice
@JathanathanEunice: W jakim sensie koordynować? Wszystko, co muszą zrobić, to ustawić pozycję pliku tak, aby była tam, gdzie chcą.
Mehrdad
1
Jeśli plik odczytuje wiele podmiotów, mogą istnieć zależności. Konieczne może być rozpoczęcie od miejsca, w którym inny przerwał (lub w miejscu określonym przez dane odczytane przez poprzedni odczyt). Ponadto czytelnicy mogą pracować w różnych wątkach, otwierając inne puszki koordynacyjne robaków. Przekazywane obiekty plików stają się stanem globalnym z wszystkimi związanymi z tym problemami (a także korzyściami).
Jonathan Eunice,
1
Kluczem nie jest przekazywanie ścieżki do pliku. Ma jedną funkcję (lub klasę, metodę lub inne miejsce kontroli) przejmującą odpowiedzialność za „pełne przetwarzanie pliku”. Jeśli dostęp do plików jest gdzieś enkapsulowany , nie musisz przekazywać zmiennego stanu globalnego, takiego jak otwarte uchwyty plików.
Jonathan Eunice,
1
W takim razie możemy zgodzić się nie zgodzić. Mówię, że istnieje zdecydowany minus projektów, które płynnie przechodzą wokół zmiennego stanu globalnego. Są też pewne zalety. Zatem „kompromis”. Projekty, które przechodzą przez ścieżki plików często wykonują operacje we / wy za jednym zamachem, w sposób enkapsulowany. Widzę to jako korzystne połączenie. YMMV.
Jonathan Eunice,
1

Chodzi o własność i odpowiedzialność za zamknięcie pliku. Możesz przekazać uchwyt strumienia lub pliku lub cokolwiek innego, co powinno zostać w pewnym momencie zamknięte / zbywane, na inną metodę, o ile upewnisz się, że jest jasne, kto jest jego właścicielem, i że po zakończeniu zostanie ono zamknięte przez właściciela . Zazwyczaj wiąże się to z konstrukcją typu wypróbowanie na końcu lub wzorem jednorazowym.

Martin Maat
źródło
-1

Jeśli zdecydujesz się przekazać otwarte pliki, możesz zrobić coś takiego, ale NIE masz dostępu do nazwy pliku w funkcji, która zapisuje do pliku.

Zrobiłbym to, gdybym chciał mieć klasę, która byłaby w 100% odpowiedzialna za operacje na plikach / strumieniach oraz inne klasy lub funkcje, które byłyby naiwne i nie spodziewałyby się otwierania lub zamykania wspomnianych plików / strumieni.

Pamiętaj, że menedżerowie kontekstu działają jak klauzula „wreszcie”. Jeśli więc w funkcji zapisującej zostanie zgłoszony wyjątek, plik zostanie zamknięty bez względu na wszystko.

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
źródło
Jak to jest lepsze / inne niż zwykłe używanie with open? Jak to rozwiązuje kwestię używania nazw plików w porównaniu z obiektami podobnymi do plików?
Dannnno
To pokazuje sposób na ukrycie zachowania otwieranie / zamykanie pliku / strumienia. Jak wyraźnie widać w komentarzach, daje to możliwość dodania logiki przed otwarciem strumienia / pliku, który jest przezroczysty dla „pisarza”. „Pisarz” może być metodą klasy innego pakietu. Zasadniczo jest to opakowanie otwarte. Dziękujemy również za odpowiedź i głosowanie.
Vls
To zachowanie jest już obsługiwane with open, prawda? A to, za czym skutecznie opowiadasz się, to funkcja, która używa tylko obiektów podobnych do plików i nie dba o to, skąd pochodzi?
Dannnno