Haruskah saya memasukkan nama file untuk dibuka, atau membuka file?

53

Misalkan saya memiliki fungsi yang melakukan hal-hal dengan file teks - misalnya membaca dari itu dan menghapus kata 'a'. Saya bisa memberikannya nama file dan menangani pembukaan / penutupan fungsi, atau saya bisa memberikannya file yang dibuka dan berharap siapa pun yang memanggilnya akan berurusan dengan menutupnya.

Cara pertama sepertinya cara yang lebih baik untuk menjamin tidak ada file yang dibiarkan terbuka, tetapi mencegah saya menggunakan hal-hal seperti objek StringIO

Cara kedua bisa sedikit berbahaya - tidak ada cara untuk mengetahui apakah file akan ditutup atau tidak, tapi saya akan dapat menggunakan objek seperti 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)

Apakah salah satu dari ini umumnya disukai? Apakah umumnya diharapkan bahwa suatu fungsi akan berperilaku dalam salah satu dari dua cara ini? Atau haruskah itu didokumentasikan dengan baik sehingga programmer dapat menggunakan fungsi yang sesuai?

Dannnno
sumber

Jawaban:

39

Antarmuka yang nyaman bagus, dan kadang-kadang cara untuk pergi. Namun, sebagian besar waktu kompabilitas yang baik lebih penting daripada kenyamanan , karena abstraksi komposabel memungkinkan kita untuk mengimplementasikan fungsionalitas lain (termasuk pembungkus kenyamanan) di atasnya.

Cara paling umum untuk fungsi Anda menggunakan file adalah dengan mengambil handle file terbuka sebagai parameter, karena ini memungkinkannya untuk juga menggunakan handle file yang bukan bagian dari sistem file (mis. Pipa, soket, ...):

def your_function(open_file):
    return do_stuff(open_file)

Jika ejaan with open(filename, 'r') as f: result = your_function(f)terlalu banyak untuk ditanyakan kepada pengguna Anda, Anda dapat memilih salah satu solusi berikut:

  • your_functionmengambil file terbuka atau nama file sebagai parameter. Jika itu adalah nama file, file dibuka dan ditutup, dan pengecualian diperbanyak. Ada sedikit masalah dengan ambiguitas di sini yang bisa diselesaikan dengan menggunakan argumen bernama.
  • Tawarkan pembungkus sederhana yang menangani membuka file, misalnya

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

    Saya biasanya melihat fungsi-fungsi seperti API mengasapi, tetapi jika mereka menyediakan fungsi yang umum digunakan, kenyamanan yang didapat adalah argumen yang cukup kuat.

  • Bungkus with openfungsionalitas dalam fungsi komposable lain:

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

    digunakan sebagai with_file(name, your_function)atau dalam kasus yang lebih rumitwith_file(name, lambda f: some_function(1, 2, f, named=4))

amon
sumber
6
Satu-satunya kelemahan dari pendekatan ini adalah bahwa kadang-kadang nama objek seperti file diperlukan, misalnya untuk pelaporan kesalahan: pengguna akhir lebih suka melihat "Kesalahan di foo.cfg (12)" daripada "Kesalahan dalam <stream @ 0x03fd2bb6> (12) ". Argumen "stream_name" opsional untuk your_functiondapat digunakan dalam hal ini.
22

Pertanyaan sesungguhnya adalah kelengkapan. Apakah fungsi pemrosesan file Anda adalah pemrosesan file yang lengkap, atau hanya sepotong dalam rangkaian langkah pemrosesan? Jika sudah lengkap dan dengan sendirinya, maka merasa bebas untuk merangkum semua akses file dalam suatu fungsi.

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

Ini memiliki properti yang sangat bagus untuk menyelesaikan sumber daya (menutup file) di akhir withpernyataan.

Namun jika ada kemungkinan untuk memproses file yang sudah terbuka, maka perbedaan Anda ver_1dan ver_2lebih masuk akal. Sebagai contoh:

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)

Semacam ini pengujian tipe eksplisit sering disukai , terutama dalam bahasa seperti Java, Julia, dan Go mana jenis-atau pengiriman berbasis antarmuka langsung didukung. Namun, dalam Python, tidak ada dukungan bahasa untuk pengiriman berbasis tipe. Terkadang Anda mungkin melihat kritik pengujian tipe langsung dalam Python, tetapi dalam praktiknya itu sangat umum dan cukup efektif. Ini memungkinkan suatu fungsi memiliki tingkat umum yang tinggi, menangani tipe data apa pun yang mungkin muncul, alias "mengetik bebek." Perhatikan garis bawah utama aktif _ver_file; itu adalah cara konvensional untuk menetapkan fungsi (atau metode) "pribadi". Meskipun secara teknis dapat dipanggil langsung, itu menunjukkan bahwa fungsi tidak dimaksudkan untuk konsumsi eksternal langsung.


Pembaruan 2019: Diberikan pembaruan terbaru dalam Python 3, misalnya jalur sekarang berpotensi disimpan sebagai pathlib.Pathobjek bukan hanya stratau bytes(3,4+), dan tipe yang mengisyaratkan telah berubah dari esoterik menjadi arus utama (sekitar 3,6+, meskipun masih aktif berkembang), inilah kode yang diperbarui yang memperhitungkan kemajuan ini:

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
sumber
1
Mengetik bebek akan menguji berdasarkan apa yang dapat Anda lakukan dengan objek, bukan apa tipenya. Misalnya, mencoba memanggil readsesuatu yang mungkin seperti file, atau memanggil open(fileobj, 'r')dan menangkap TypeErrorif fileobjbukan string.
user2357112
Anda berdebat untuk mengetik bebek yang digunakan . Contoh ini memberikan efek mengetik bebek - yaitu, pengguna mendapatkan veroperasi independen dari jenis. Mungkin juga bisa diterapkan vermelalui mengetik bebek, seperti yang Anda katakan. Tapi menghasilkan pengecualian kemudian lebih lambat dari inspeksi tipe sederhana, dan IMO tidak menghasilkan manfaat tertentu (kejelasan, generalisasi, dll.) Dalam pengalaman saya, mengetik bebek adalah luar biasa "dalam jumlah besar," tetapi netral untuk kontraproduktif "dalam skala kecil . "
Jonathan Eunice
3
Tidak, apa yang Anda lakukan masih bukan mengetik bebek. Sebuah hasattr(fileobj, 'read')tes akan mengetik bebek; sebuah isinstance(fileobj, str)tes tidak. Inilah contoh perbedaannya: isinstancetes gagal dengan nama file unicode, karena u'adsf.txt'bukan a str. Anda telah menguji jenis yang terlalu spesifik. Tes mengetik bebek, apakah berdasarkan panggilan openatau does_this_object_represent_a_filenamefungsi hipotetis , tidak akan memiliki masalah itu.
user2357112
1
Jika kode tersebut adalah kode produksi dan bukan contoh penjelas, saya juga tidak akan memiliki masalah itu, karena saya tidak akan menggunakan is_instance(x, str)tetapi lebih seperti is_instance(x, string_types), dengan string_typesdiatur dengan benar untuk operasi yang tepat di PY2 dan PY3. Mengingat sesuatu yang dukun seperti tali, verakan bereaksi dengan benar; diberi sesuatu yang dukun seperti file, sama. Untuk sebuah pengguna dari ver, tidak akan ada perbedaan - kecuali bahwa pelaksanaan jenis pemeriksaan akan berjalan lebih cepat. Bebek puritan: jangan ragu untuk tidak setuju.
Jonathan Eunice
5

Jika Anda memberikan nama file di sekitar alih-alih menangani file maka tidak ada jaminan bahwa file kedua adalah file yang sama dengan yang pertama saat dibuka; ini dapat menyebabkan bug dan lubang keamanan benar.

Mehrdad
sumber
1
Benar. Tapi itu harus diimbangi dengan pengorbanan lain: Jika Anda mengoper file, semua pembaca harus mengoordinasikan akses mereka ke file, karena masing-masing cenderung memindahkan "posisi file saat ini."
Jonathan Eunice
@ JonathanEunice: Berkoordinasi dalam arti apa? Yang perlu mereka lakukan adalah mengatur posisi file di mana pun mereka inginkan.
Mehrdad
1
Jika ada beberapa entitas yang membaca file, mungkin ada dependensi. Seseorang mungkin perlu memulai dari yang lainnya (atau di tempat yang ditentukan oleh data yang dibaca oleh pembacaan sebelumnya). Juga, pembaca mungkin berjalan di utas berbeda, membuka kaleng koordinasi cacing lainnya. Objek file yang dilewati menjadi keadaan global yang terbuka, dengan semua masalah (serta manfaat) yang menyertainya.
Jonathan Eunice
1
Itu tidak melewati jalur file yang kuncinya. Ini memiliki satu fungsi (atau kelas, metode, atau locus of control lainnya) yang bertanggung jawab atas "pemrosesan file secara lengkap." Jika akses file diringkas di suatu tempat , maka Anda tidak perlu melewati keadaan global yang bisa berubah seperti pegangan file terbuka.
Jonathan Eunice
1
Yah, kita bisa sepakat untuk tidak setuju kalau begitu. Saya mengatakan ada kelemahan yang diputuskan untuk desain yang dengan mudah melewati keadaan global yang bisa berubah. Ada beberapa keuntungan juga. Jadi, "pertukaran". Desain yang melewati jalur file sering dilakukan I / O dalam sekali gerakan, dengan cara enkapsulasi. Saya melihat itu sebagai kopling yang menguntungkan. YMMV.
Jonathan Eunice
1

Ini tentang kepemilikan dan tanggung jawab untuk menutup file. Anda dapat meneruskan aliran atau file menangani atau hal apa pun yang harus ditutup / dibuang di beberapa titik ke metode lain, selama Anda memastikan jelas siapa yang memilikinya dan yakin itu akan ditutup oleh pemilik ketika Anda selesai . Ini biasanya melibatkan konstruksi try-akhirnya atau pola pakai.

Martin Maat
sumber
-1

Jika Anda memilih untuk meneruskan file yang terbuka, Anda dapat melakukan sesuatu seperti TETAPI berikut ini, Anda tidak memiliki akses ke nama file dalam fungsi yang menulis ke dalam file.

Saya akan melakukan ini jika saya ingin memiliki kelas yang 100% bertanggung jawab atas operasi file / stream dan kelas atau fungsi lain yang akan naif dan tidak diharapkan untuk membuka atau menutup file / stream tersebut.

Ingat bahwa manajer konteks bekerja seperti memiliki klausa akhirnya. Jadi, jika pengecualian dilemparkan dalam fungsi penulis, file akan ditutup tidak peduli apa.

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
sumber
Bagaimana ini lebih baik / berbeda dari hanya menggunakan with open? Bagaimana ini menjawab pertanyaan tentang menggunakan nama file vs objek seperti file?
Dannnno
Ini menunjukkan kepada Anda cara untuk menyembunyikan file / stream perilaku buka / tutup. Seperti yang Anda lihat dengan jelas di komentar itu memberi Anda cara untuk menambahkan logika sebelum membuka aliran / file yang transparan untuk "penulis". "Penulis" bisa menjadi metode kelas dari paket lain. Intinya itu adalah pembungkus terbuka. Juga, terima kasih telah membalas dan memberikan suara.
Vls
Perilaku itu sudah ditangani with open, kan? Dan yang Anda advokasi secara efektif adalah fungsi yang hanya menggunakan objek seperti file, dan tidak peduli dari mana asalnya?
Dannnno