web-dev-qa-db-ja.com

RAII in Python-スコープを離れるときの自動破棄

私はPythonで [〜#〜] raii [〜#〜] を見つけようとしてきました。 Resource Allocation Is Initializationは、オブジェクトが作成されるときにオブジェクトが初期化されるC++のパターンです。失敗すると、例外がスローされます。このようにして、プログラマーは、オブジェクトが半分構築された状態のままになることは決してないことを知っています。 Pythonこれだけのことができます。

ただし、RAIIはC++のスコープ規則とも連携して、オブジェクトの迅速な破棄を保証します。変数がスタックから外れるとすぐに破棄されます。これはPythonで発生する可能性がありますが、外部参照または循環参照がない場合に限ります。

さらに重要なことに、オブジェクトの名前は、オブジェクトが存在する関数が終了するまで(場合によってはそれより長く)存在し続けます。モジュールレベルの変数は、モジュールの存続期間中は存続します。

次のようなことをするとエラーが発生します:

for x in some_list:
    ...

... 100 lines later ...

for i in x:
    # Oops! Forgot to define x first, but... where's my error?
    ...

使用後に手動で名前を削除することもできますが、それはかなり醜く、私の側で努力が必要です。

この場合、Do-What-I-Meanを実行したいと思います。

for x in some_list:
    surface = x.getSurface()
    new_points = []
    for x,y,z in surface.points:
        ...     # Do something with the points
        new_points.append( (x,y,z) )
    surface.points = new_points
    x.setSurface(surface)

Pythonはいくつかのスコープを実行しますが、インデントレベルではなく、機能レベルでのみ実行します。名前を再利用できるように、変数のスコープを設定するためだけに新しい関数を作成する必要があるのはばかげているようです。

Python 2.5には "with"ステートメント がありますが、これには__enter__関数と__exit__関数を明示的に挿入する必要があり、一般に、ファイルやミューテックスロックなどのリソースのクリーンアップに向いているようです。出口ベクトルの。スコーピングには役立ちません。それとも私は何かが足りないのですか?

「PythonRAII」と「Pythonスコープ」を検索しましたが、問題に直接かつ信頼できる方法で対処するものは見つかりませんでした。私はすべてのPEPを調べました。この概念はPython内では扱われていないようです。

Pythonでスコープ変数が欲しいので、私は悪い人ですか?それはあまりにも非Pythonicですか?

私はそれを食べていませんか?

おそらく私は、言語の動的な側面の利点を取り除こうとしています。スコープを強制したい場合があるのは利己的ですか?

コンパイラー/インタープリターに私の怠慢な変数の再利用の間違いを見つけてもらいたいのは怠惰ですか?ええ、もちろん、私は怠け者ですが、悪い意味で怠け者ですか?

33
markets

tl; dr RAIIは不可能です。一般的にスコープと混同し、これらの余分なスコープを見逃すと、おそらく悪いコードを書いていることになります。

おそらく私はあなたの質問を受け取らないか、Pythonについていくつかの非常に重要なことを理解していません...まず、スコープに関連付けられた決定論的なオブジェクトの破壊は不可能ですガベージコレクションされた言語で。 Pythonの変数は単なる参照です。ポインタが指すとすぐに、メモリのmalloc 'dチャンクがfree'されることは望ましくありません。範囲外になりますか?参照カウントを使用する場合のいくつかの状況での実用的な例外-しかし、正確に設定するのに十分な言語はありません石での実装。

そしてたとえCPythonのように参照カウントがあるとしても、それは実装の詳細です。一般に、参照カウントを使用せずにさまざまな実装notがあるPython)に含めると、すべてのオブジェクトがぶら下がっているようにコーディングする必要がありますメモリがなくなるまで。

関数呼び出しの残りの部分に存在する名前の場合:delステートメントを使用して、現在のスコープまたはグローバルスコープから名前をcan削除できます。 。ただし、これは手動のメモリ管理とは関係ありません。参照を削除するだけです。これは、参照されるオブジェクトがGCされるようにトリガーする場合としない場合があり、演習のポイントではありません。

  • コードがこれで名前の衝突を引き起こすのに十分な長さである場合は、より小さな関数を作成する必要があります。また、わかりやすく、衝突しにくい名前を使用してください。ネストされたループが出力ループの反復変数を上書きする場合も同じです。まだこの問題に遭遇していないので、名前が十分に説明されていないか、これらのループを分解する必要がありますか?

正解です。withはスコープとは関係がなく、決定論的なクリーンアップとは関係ありません(したがって、最終的にはRAIIと重複しますが、手段では重複しません)。

おそらく私は、言語の動的な側面の利点を取り除こうとしています。スコープを強制したい場合があるのは利己的ですか?

いいえ。まともな字句スコープは、動的/静的性に依存しないメリットです。確かに、Python(2-3はこれをほぼ修正しました)にはこの点で弱点がありますが、クロージャの領域にあります。

しかし、「なぜ」を説明するために:Pythonmust宣言なしで別のことを言っているので、新しいスコープを開始する場所を控えめにする必要があります、名前に割り当てると、その名前は最も内側/現在のスコープに対してローカルになります。たとえば、forループに独自のスコープがある場合、ループ外の変数を簡単に変更することはできません。

コンパイラー/インタープリターに私の怠慢な変数の再利用の間違いを見つけてもらいたいのは怠惰ですか?ええ、もちろん、私は怠け者ですが、悪い意味で怠け者ですか?

繰り返しになりますが、(エラーや落とし穴をもたらすような方法で)名前を誤って再利用することはまれであり、とにかく小さなことだと思います。

編集:これをできるだけ明確にもう一度述べるには:

  • GCを使用する言語では、スタックベースのクリーンアップはできません。定義上、可能性はほとんどありません。変数は、ヒープ上のオブジェクトへの潜在的に多くの参照の1つであり、いつかはわかりません。変数はスコープ外になり、すべてのメモリ管理はGCの手に委ねられます。GCは、スタックフレームがポップされたときではなく、必要なときに実行されます。リソースのクリーンアップは別の方法で解決されます。以下を参照してください。
  • 確定的クリーンアップはwithステートメントを介して行われます。はい、新しいスコープは導入されません(以下を参照)。これは目的ではないためです。管理対象オブジェクトがバインドされている名前が削除されても、削除されません。クリーンアップは行われましたが、残っているのは「使用できない私に触れないでください」オブジェクト(閉じたファイルストリームなど)です。
  • Pythonには、関数、クラス、モジュールごとにスコープがあります。期間。好きかどうかに関係なく、このように言語が機能します。よりきめ細かいスコープが必要/「必要」な場合は、コードをよりきめ細かい関数に分割します。よりきめ細かいスコープが必要な場合もありますが、そうではありません。この回答の前半で指摘した理由(「編集:」の上の3つの段落)により、これには理由があります。好むと好まざるとにかかわらず、これが言語の仕組みです。
30
user395760
  1. あなたはwithについて正しいです-それは変数のスコープとは完全に無関係です。

  2. 問題があると思われる場合は、グローバル変数を避けてください。これには、モジュールレベルの変数が含まれます。

  3. Pythonで状態を非表示にする主なツールはクラスです。

  4. ジェネレータ式(およびPython 3でもリスト内包表記)には独自のスコープがあります。

  5. 関数がローカル変数を見失うほど長い場合は、おそらくコードをリファクタリングする必要があります。

14
Sven Marnach

ただし、RAIIはC++のスコープ規則とも連携して、オブジェクトの迅速な破棄を保証します。

これは、メモリが 代替可能 であるという考えに基づいているGC言語では重要でないと見なされます。新しいオブジェクトを割り当てるのに十分なメモリが他の場所にある限り、オブジェクトのメモリを再利用する差し迫った必要はありません。ファイルハンドル、ソケット、ミューテックスなどの代替不可能なリソースは、特別に処理される特殊なケースと見なされます(例:with)。これは、すべてのリソースを同じように扱うC++のモデルとは対照的です。

変数がスタックから外れるとすぐに破棄されます。

Pythonにはスタック変数がありません。 C++の用語では、すべてshared_ptr

Pythonはいくつかのスコープを実行しますが、インデントレベルではなく、機能レベルでのみ実行します。名前を再利用できるように、変数のスコープを設定するためだけに新しい関数を作成する必要があるのはばかげているようです。

それまたはジェネレーター理解レベルで(そして3.xではall理解で)スコープを行います。

forループ変数を壊したくない場合は、それほど多くのforループを使用しないでください。特に、ループでappendを使用するのは非Pythonicです。の代わりに:

new_points = []
for x,y,z in surface.points:
    ...     # Do something with the points
    new_points.append( (x,y,z) )

書く:

new_points = [do_something_with(x, y, z) for (x, y, z) in surface.points]

または

# Can be used in Python 2.4-2.7 to reduce scope of variables.
new_points = list(do_something_with(x, y, z) for (x, y, z) in surface.points)
9
dan04

基本的に、あなたはおそらく間違った言語を使用しています。正しいスコープルールと信頼性の高い破棄が必要な場合は、C++を使用するか、Perlを試してください。メモリがいつ解放されるかについてのGCの議論は、要点を見逃しているようです。ミューテックスやファイルハンドルなどの他のリソースを解放することです。 C#は、参照カウントがゼロになったときに呼び出されるデストラクタと、メモリをリサイクルすることを決定したときに呼び出されるデストラクタを区別していると思います。人々はメモリのリサイクルについてそれほど心配していませんが、参照されなくなったらすぐに知りたいと思っています。 Pythonは言語としての本当の可能性を持っていたので残念です。しかし、それは型破りなスコープであり、信頼性の低いデストラクタ(または少なくとも実装に依存するもの)は、C++とPerlで得られる力を否定されていることを意味します。

GCで古いメモリをリサイクルするのではなく、利用可能な場合は新しいメモリを使用することについての興味深いコメント。それはメモリリークを言うための単なる空想的な言い方ではありません:-)

2
tony

何年にもわたるC++の後でPythonに切り替えると、ファイルや接続を閉じるなど、RAIIタイプの動作を模倣するために__del__に頼りたくなることがわかりました。ただし、状況があります。 (たとえば、Rxによって実装されたオブザーバーパターン)監視対象がオブジェクトへの参照を維持し、オブジェクトを存続させます!したがって、ソースによって終了される前に接続を閉じたい場合は、試行してもどこにも到達しません__del__でそれを行います。

UIプログラミングでは、次の状況が発生します。

class MyComponent(UiComponent):

    def add_view(self, model):
        view = TheView(model) # observes model
        self.children.append(view)

    def remove_view(self, index):
        del self.children[index] # model keeps the child alive

したがって、RAIIタイプの動作を取得する方法は次のとおりです。追加フックと削除フックを使用してコンテナーを作成します。

import collections

class ScopedList(collections.abc.MutableSequence):

    def __init__(self, iterable=list(), add_hook=lambda i: None, del_hook=lambda i: None):
        self._items = list()
        self._add_hook = add_hook
        self._del_hook = del_hook
        self += iterable

    def __del__(self):
        del self[:]

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, item):
        self._del_hook(self._items[index])
        self._add_hook(item)
        self._items[index] = item

    def __delitem__(self, index):
        if isinstance(index, slice):
            for item in self._items[index]:
                self._del_hook(item)
        else:
            self._del_hook(self._items[index])
        del self._items[index]

    def __len__(self):
        return len(self._items)

    def __repr__(self):
        return "ScopedList({})".format(self._items)

    def insert(self, index, item):
        self._add_hook(item)
        self._items.insert(index, item)

UiComponent.childrenScopedListであり、子に対してacquireメソッドとdisposeメソッドを呼び出す場合、使用されているのと同じように、確定的なリソースの取得と破棄が保証されます。 C++で。

2
Jonathan Zrake