開くファイル名を渡すべきですか、それともファイルを開くべきですか?


53

テキストファイルを処理する関数があるとします。たとえば、テキストファイルから読み取り、単語 'a'を削除します。ファイル名を渡して関数の開始/終了を処理するか、開いたファイルを渡して、それを呼び出す人がそれを閉じることに対処することを期待できます。

最初の方法は、ファイルが開いたままにならないことを保証するより良い方法のように思えますが、StringIOオブジェクトのようなものを使用することを防ぎます

2番目の方法は少し危険です-ファイルが閉じられるかどうかを知る方法はありませんが、ファイルのようなオブジェクトを使用することができます

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)

これらのいずれかが一般的に好まれていますか?一般に、関数はこれらの2つの方法のいずれかで動作すると予想されますか?または、プログラマーが関数を適切に使用できるように、適切に文書化する必要がありますか?

回答:


39

便利なインターフェースは便利で、時には進むべき道でもあります。ただし、コンポーザブルな抽象化により、そのに他の機能(コンビニエンスラッパーを含む)を実装できるため、ほとんどの場合、コンビネーションよりも優れたコンポーザビリティが重要です

関数がファイルを使用する最も一般的な方法は、開いているファイルハンドルをパラメーターとして使用することです。これにより、ファイルシステムの一部ではないファイルハンドル(パイプ、ソケットなど)も使用できるようになります。

def your_function(open_file):
    return do_stuff(open_file)

with open(filename, 'r') as f: result = your_function(f)ユーザーに尋ねるにはスペルが多すぎる場合は、次の解決策のいずれかを選択できます。

  • your_functionパラメータとして開いているファイルまたはファイル名を取ります。ファイル名の場合、ファイルが開かれ、閉じられ、例外が伝播されます。ここには曖昧さに関する問題が少しありますが、名前付き引数を使用すると回避できます。
  • ファイルを開く処理を行う単純なラッパーを提供します。例えば

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

    私は一般にそのような関数をAPIの肥大化として認識していますが、それらが一般的に使用される機能を提供する場合、得られる利便性は十分に強力な議論です。

  • with open機能を別の構成可能な関数でラップします。

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

    使用するwith_file(name, your_function)か、より複雑な場合にはwith_file(name, lambda f: some_function(1, 2, f, named=4))


6
このアプローチの唯一の欠点は、ファイルのようなオブジェクトの名前が必要な場合があることです。たとえば、エラー報告の場合:エンドユーザーは「<stream @ 0x03fd2bb6>のエラー」ではなく「Error.foo.cfg(12)」 (12)」。your_functionこれに関して、オプションの「stream_name」引数を使用できます。

22

本当の問題は完全性です。ファイル処理機能はファイルの完全な処理ですか、それとも一連の処理ステップの一部ですか?それ自体で完全な場合は、関数内のすべてのファイルアクセスを自由にカプセル化してください。

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

これには、withステートメントの最後でリソースをファイナライズする(ファイルを閉じる)という非常に優れたプロパティがあります。

ただし、既に開いているファイルを処理する必要がある可能性がある場合は、あなたver_1との区別ver_2がより意味があります。例えば:

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)

この種の明示的な型テストは、特にJava、Julia、Goなどの型またはインターフェイスベースのディスパッチが直接サポートされている言語では、しばしば嫌われます。ただし、Pythonでは、型ベースのディスパッチの言語サポートはありません。Pythonで直接型テストを批判することもありますが、実際には非常に一般的で非常に効果的です。これにより、関数に高度な汎用性を持たせ、「ダックタイピング」とも呼ばれるデータ型が処理されるようになります。の先頭のアンダースコアに注意してください_ver_file。これは、「プライベート」関数(またはメソッド)を指定する従来の方法です。技術的には直接呼び出すことができますが、関数は直接的な外部消費を目的としていないことを示唆しています。


2019アップデート:パスは今、潜在的として格納されていることを、たとえば、Pythonの3に最新の更新を考えるpathlib.Pathだけではなく、オブジェクトstrまたはbytes(3.4+)、及び(まだ積極的に進化しても、年頃3.6以降)、そのタイプヒンティングは難解から主流になった、ここでのこれらの進歩を考慮した更新コード:

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
アヒルのタイピングは、そのタイプが何であるかではなく、オブジェクトで何ができるかに基づいてテストします。たとえば、readファイルのようなものを呼び出しようopen(fileobj, 'r')としたり、TypeErrorif fileobjが文字列でない場合に呼び出してキャッチします。
user2357112 14

あなたは使用中のアヒルタイプを主張しています。この例では、アヒルのタイピングが有効になっています。つまり、ユーザーverはタイプに依存しない操作を取得します。verあなたが言うように、アヒルのタイピングを通して実装することも可能かもしれません。しかし、例外を生成してキャッチするのは単純な型検査よりも遅く、IMOは特定の利点(明確性、一般性など)をもたらしません。 」
ジョナサンユニス14

3
いいえ、あなたがまだやっていることは、アヒルのタイピングではありません。hasattr(fileobj, 'read')テストでは、鴨タイピングだろう。isinstance(fileobj, str)テストではありません。ここでの違いの例です:isinstanceテストは、Unicodeのファイル名で失敗し、以降u'adsf.txt'ではありませんstr。あまりにも特定のタイプをテストしました。アヒルのタイピングテストは、呼び出しに基づいていようとopen仮想does_this_object_represent_a_filename関数に基づいていようと、その問題はありません。
user2357112 14

1
コードが説明用の例ではなく本番用のコードである場合、PY2とPY3で適切に動作is_instance(x, str)するようis_instance(x, string_types)string_types適切に設定されたを使用しますが、文字列のように鳴る何かが与えられるverと、適切に反応します。同じように、ファイルのように鳴る何かを与えられます。ユーザーver型検査の実装は高速に実行することを除いて- 、何の違いもないでしょう。アヒルの純粋主義者:自由に意見が分かれます。
ジョナサンユニス14

5

ファイルハンドルの代わりにファイル名を渡すと、2番目のファイルが開かれたときに最初のファイルと同じファイルであるという保証はありません。これは、正確性のバグとセキュリティホールにつながる可能性があります。


1
本当です。ただし、別のトレードオフと相殺する必要があります。ファイルハンドルを渡す場合、すべてのリーダーはファイルへのアクセスを調整する必要があります。
ジョナサンユニス14

@JonathanEunice:どのような意味で調整しますか?必要なのは、ファイルの位置を希望する場所に設定することだけです。
Mehrdad 14

1
ファイルを読み取るエンティティが複数ある場合、依存関係がある可能性があります。別の人が中断したところから開始する必要がある場合があります(または前の読み取りによって読み取られたデータによって定義された場所)。また、読者は異なるスレッドで実行され、ワー​​ムの他の調整缶を開く可能性があります。渡されたファイルオブジェクトは、すべての問題(および利点)を伴うグローバルな状態に公開されます。
ジョナサンユニス14

1
キーであるファイルパスを迂回することはありません。1つの関数(またはクラス、メソッド、またはその他の制御領域)が「ファイルの完全な処理」を担当します。ファイルアクセスがどこかでカプセル化されいる場合、開いているファイルハンドルのような可変グローバル状態を渡す必要はありません。
ジョナサンユニス14

1
さて、私たちは同意しないことに同意することができます。可変のグローバルステートを一見して渡すデザインには、決定的な欠点があると言っています。いくつかの利点もあります。したがって、「トレードオフ」。ファイルパスを渡すデザインは、多くの場合、カプセル化された方法で一気にI / Oを実行します。それは有利なカップリングだと思います。YMMV。
ジョナサンユニス

1

これは、所有権とファイルを閉じる責任についてです。あなたは、それはそれを所有する人明確かつ一定である限り、あなたは確認してくださいとして、他の方法にはいくつかの点で/に配置された閉鎖されるべきブツストリームまたはファイルハンドルまたは何を渡すことができます設定が終了したら、所有者によって閉鎖されます。これには通常、最終的な構築物または使い捨てパターンが含まれます。


-1

開いているファイルを渡すことを選択した場合、次のようなことができますが、ファイルに書き込む関数のファイル名にはアクセスできません。

ファイル/ストリーム操作と、そのファイル/ストリームを開いたり閉じたりすることを期待しない素朴な他のクラスまたは機能を100%担当するクラスが必要な場合、これを行います。

コンテキストマネージャは、finally句を持つように機能することを忘れないでください。そのため、ライター関数で例外がスローされた場合、ファイルは何であれ閉じられます。

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)

これは単に使用するよりも優れている/異なるのwith openですか?これは、ファイル名とファイルのようなオブジェクトを使用する問題にどのように対処しますか?
ダンノ

これは、ファイル/ストリームのオープン/クローズ動作を非表示にする方法を示しています。コメントではっきりわかるように、「ライター」に対して透過的なストリーム/ファイルを開く前にロジックを追加する方法を提供します。「ライター」は、別のパッケージのクラスのメソッドである可能性があります。本質的に、それはopenのラッパーです。また、返信および投票していただきありがとうございます。
Vls

with openただし、その動作は既に処理されていますよね?そして、あなたが効果的に提唱しているのは、ファイルのようなオブジェクトのみを使用し、それがどこから来たのかを気にしない関数です。
ダンノ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.