Pythonコアテクノロジーと実戦-二一|コンテキストマネージャとwith文でコードを簡素化

14595 ワード

私たちはPythonの中でwithの文についてよく知られていないはずです.特にファイルの入出力操作では、具体的な使用過程で、どんな引伸の意味がありますか.これに密接に関連するコンテキストマネージャ(context manager)は何ですか.
コンテキストマネージャとは
いずれのプログラミング言語でも、ファイルの入出力、データベースの接続の確立、切断などの操作は、一般的なリソース管理操作です.しかし、リソースは限られています.プログラムを書くときは、これらのリソースが使用後に解放されることを保証しなければなりません.そうしないと、リソースの漏洩を引き起こしやすく、軽者システムの処理が遅く、重ければシステムが崩壊します.
例を見てみましょう
for i in range(100000000):
    f = open('test.txt','w')
    f.write('hello')

ループで10000000個のファイルを開きましたが、使用後に閉じる操作は行われず、コードを実行するとエラーが発生しました.
これは典型的なリソース漏洩の例であり、プログラムで同時に多くのファイルを開き、多くのリソースを占有し、クラッシュをもたらすためです.
この問題を解決するために,異なるプログラミング言語には異なるメカニズムが導入されており,Pythonでは対応する解決策がコンテキストマネージャ(context manager)である.コンテキストマネージャは、リソースを自動的に割り当てて解放することができます.最も典型的なアプリケーションはwith文なので、上のコードはこのように書くべきです.
for x in range(100000000):
    with open('test.txt','w') as f:
        f.write('hello world')

これにより、ファイル「test.txt」を開いて文字を書き込むたびに、このファイルは自動的に閉じられ、対応するリソースも解放され、リソースの漏洩を防ぐことができます.もちろん、withの文も以下のように表すことができます.
f = open('test.txt','w')
try:
    f.write('hello world')
finally:
    f.close()

ここではfinallyのプログラムセグメントに注意しなければなりません.書き込み中に異常が発生しても、ファイルが最終的に閉じられることを保証することができます.withを比較しすぎないと冗長に見え、finallyも無視されやすいので、私たちは普段with文を使う傾向があります.
もう1つの典型的な例は、Pythonのスレッドロック(threrading.lockクラス)です.例えば、ロックを取得し、対応する操作を実行してから解放するには、コードはこのようなものです.
import threading
some_lock = threading.Lock()
some_lock.acquire()
try:
    pass
finally:
    some_lock.release()

それに対応するwith文は非常に簡潔です
import threading
some_lock = threading.Lock()
with some_lock:
    pass

上記の2つの例から,with文を用いることで,コード構造を大幅に簡素化し,リソース漏洩の発生を効果的に回避できることが分かった.
コンテキストマネージャの実装
クラスベースのコンテキストマネージャ
コンテキスト管理の概念と利点を受け取った後、私たちは以下の例を通じて、コンテキストマネージャの原理を見て、彼の内部をハイビジョンして実現します.ここでは、Pythonのファイルを開く、閉じる操作をシミュレートするコンテキスト管理クラスFileManagerを定義します.
class FileManager():
    def __init__(self,name,mode):
        print('call __init__ method')
        self.name = name
        self.mode = mode
        self.file = None

    def __enter__(self):
        print('calling __enter__ method')
        self.file = open(self.name,self.mode)
        return self.file
    def __exit__(self,exc_type,exc_val,exc_tb):
        print('call __exit__ method')
        if self.file:
            self.file.close()

with FileManager('test.txt','w') as f:
    print('ready to write to file')
    f.write('hello world')

##########  ##########
call __init__ method
calling __enter__ method
ready to write to file
call __exit__ method

特に、クラスを使用してコンテキストマネージャを作成する場合は、次の2つの方法が含まれていることを確認する必要があります.
__enter__()
__exit__()

さらにenterメソッドは、管理する必要があるリソースを返します.メソッドexitには、上記のコードのファイルを閉じるなど、リソースを解放したり整理したりする操作が通常あります.
with文で上記のコンテキストマネージャを実行すると、次の4つのステップが発生します.
1.構築方法_init__()が呼び出され、プログラムはオブジェクトFileManagerを初期化し、ファイル名と操作方法が入力されます.
2.方法_enter__()が呼び出され、ファイルが書き込みモードで開かれ、FileManagerに戻ってシーンに変数fを付与する
3.文字列がファイルに書き込まれる
4.方法_exit__()が呼び出され、前に開いたファイルストリームが閉じられます.
上記の出力結果が表示されます.
また、exit関数にはいくつかのパラメータが渡されています.exc_type.exc_val,exc_tb、それぞれexception_を表すtype,exception_valueとtraceback.コンテキストマネージャを含むwith文を実行すると、例外が投げ出されると、上記の3つのパラメータに例外の情報が含まれ、__に渡されます.exit__()関数です.
したがって、いくつかの異常を処理する必要がある場合は、_exit__()関数に対応するコードを追加
    def __exit__(self,exc_type,exc_val,exc_tb):
        print('call __exit__ method')
        if exc_type:
            print(f'exc_type:{exc_type}')
            print(f'exc_value:{exc_val}')
            print(f'exc_traceback:{exc_tb}')
            print('exception handled')
            return True
        if self.file:
            self.file.close()

with FileManager('test.txt','w') as f:
    raise Exception('exception raised').with_traceback(None)

exit()メソッドを変更した後、with文でraiseで例外を手動で投げ出し、コードに次の出力が表示されます.
call __init__ method
calling __enter__ method
call __exit__ method
exc_type:<class 'Exception'>
exc_value:exception raised
exc_traceback:
exception handled

注意したいのは、exit関数がTrueを返さなければ、異常は放出されます.異常が処理されたと判断した場合、exitに最後にTrueの戻り値を加算します.
 
同様に、データベースの接続などの操作においても、コンテキストマネージャを用いて表すことがよくあります.以下は簡略化されたコードです.
class DBConnectionManager():
    def __init__(self,hostname,port):
        self.hostname = hostname
        self.port = port
        self.connection = None
    
    def  __enter__(self):
        self.connection = DBClient(self.hostname,self.port)
        return self

    def __exit__(self,exc_type,exc_val,exc_tb):
        self.connection.close()


with DBConnectionManager('localhost','8080') as db_client:
    pass

コードの具体的な意味は上記の例と類似しており,詳細には説明しない.DBConnectionManagerというクラスを書き終えた後、データベース接続を確立するたびにwith文を簡単に利用すればよいので、データベースの閉鎖や異常などに関係なく、開発効率を大幅に向上させることができます.
ジェネレータベースのコンテキストマネージャ
上記のクラスベースのコンテキストマネージャはPythonで広く利用されており、多くのプロジェクトで見ることができますが、Pythonのコンテキストマネージャはこれに限らず、畜類はクラスベースであり、ジェネレータベースで実現することもできます.次の例を見てみましょう.
私たちは装飾器を使っています.contextmanagerは、with文をサポートするために必要なジェネレータベースのコンテキストマネージャを定義します.同様に、前のFileManagerで説明します.
from contextlib import contextmanager

@contextmanager
def file_manager(name,mode):
    try:
        f = open(name,mode)
        yield f
    finally:
        f.close()

with file_manager('test.txt','w') as f:
    f.write('hello world')

このコードでは、関数file_manager()は、with文を実行するとファイルが開き、ファイルオブジェクトfに戻り、with文の実行が完了するとfinallyコードセグメントの閉じる操作が実行されるジェネレータです.
ジェネレータベースのコンテキストマネージャを使用する場合、定義する必要はありません.enter__()と_exit__()2つの関数ですが、アクセサリーを付けなければならない点は非常に漏れやすいです.
この2つのコンテキストマネージャについて説明した後、クラスベースでもジェネレータベースのコンテキストマネージャでも、機能的には同じですが、次の2つにすぎません.
1.クラスベースのコンテキストマネージャがより柔軟で、大規模なシステム開発に適している
2.ジェネレータベースのコンテキストマネージャは、中小規模プログラムに適した便利で簡潔です.
しかし、いずれを使用しても、exit関数やfinallyにリソースを解放するコードを書くことが重要です.
まとめ
この章の冒頭では、リソース漏洩の発生しやすい特性とその結果を簡単な例で理解し、コンテキストマネージャという概念を導入しました.
コンテキストマネージャは、通常、ファイルのIO操作やデータベースの接続が閉じるなどのシーンで使用され、使用済みのリソースが迅速に解放されることを確保し、プログラムのセキュリティを効果的に向上させることができます.
次に、カスタムコンテキストマネージャの例を使用して、コンテキストマネージャの動作の原理を概説し、クラスベースのコンテキストマネージャとジェネレータベースのコンテキストマネージャについて説明します.両方の機能は同じで、どちらを使用するかはシーンに応じて選択します.
また、コンテキストマネージャは通常withとともに使用され、プログラムの簡潔さを大幅に向上させます.with文を使用してコンテキスト操作を実行する場合、例外が投げ出されると、例外のタイプ、値などの拘束情報がパラメータによって__に伝達されることに注意してください.exit__()関数は、関連する操作を自分で定義できますが、例外処理が完了した後、プログラムの実行を保証するためにreturn True文を必ず追加してください.そうしないと、例外が放出されます.