DjangoBrothers BLOG ✍️

2019/07/07

このエントリーをはてなブックマークに追加
Python

【Python】with文の構造を理解する

この記事ではPythonのwith文について解説します。

with文はファイルの読み書きなどでよく利用されますが、その本質を掴んでおくと、簡潔な記述ができるようになるため、ぜひ抑えておきましょう。

with文とは?

with文とは、例外処理をハンドリングするために利用されるPythonの構文で、特定の処理の前処理と後処理を設定することで、その処理をより簡潔かつ安全に利用できるようにするものと表現することができます。

この構文を利用することで、ファイルなどのよく利用するデータの扱いをスッキリ書くことができるようになります。

まずはファイルの取り扱いを用いてwith文の概念を簡単に説明します。

Pythonでのファイルの扱いは、以下のようなステップに分けられます。

  1. 指定されたファイルを開く(前処理)
  2. ファイルを用いた処理をする(本処理)
  3. 指定されたファイルを閉じる(後処理)

「ファイルを開く処理」、「ファイルを閉じる処理」は、毎回やらなければいけない処理です。

with文を使わない場合は、3つ処理を各々記述しなければなりません。しかし、with文を使えばよりシンプルな書き方をすることができます。

まずは、with文を使わない例です。ファイルはopen()という組み込み関数を使って開くことができます。 指定した名前のファイルの中身をprint()で表示してみます。

sample.py

try:
    f = open('sample.txt', 'r') # 前処理
    print(f.read())                    # 本処理
finally:
    f.close()                     # 後処理

このスクリプトは、sample.txtというファイルの中身を表示しています。 ファイルをうまく開けた場合にはちゃんとファイルを閉じる必要があるので、finally 句でclose()関数を呼び出しています。

続いて、with文を用いた書き方に変えてみます。

sample.py

with open('sample.txt', 'r') as f:
    print(f.read())

open()という関数はwith文の処理に対応しているので、上記のように書き直すことができます。

with文を使うことで、withブロックを抜けたときに自動的にファイルのclose()関数が呼び出されます。close()関数の書き忘れを防止した上で簡潔にファイルの処理を書くことができています。

また、仮に処理の途中で例外が発生してしまった場合にも、with文の中の処理であればファイルの閉じ忘れもなく、メモリの解放も行ってくれるため、効率的な処理にすることができます。

with文に対応するObjectを定義する

ファイルの処理で利用したopen()関数はPythonの組み込み関数(定義済み関数)で、デフォルトでwith文に対応しているため、特に事前準備がなくてもwith文を使うことができました。

with文に対応するようなオブジェクトは自分でも独自に作成することができます。 open()関数と似たような処理を、自分で書いてみることにします。

with文で利用できるClassを作成する条件は、__enter__メソッドと__exit__メソッドを実装することです。

ファイルの処理を参考に、簡単なサンプルを作ってみます。

sample2.py

class MyFileReader:
    def __init__(self, filename):
        print('init')
        self.filename = filename
        self.file = None

    def __enter__(self):
        """ with文で最初に呼ばれる処理 """
        print('enter')
        self.file = open(self.filename, 'r')
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        """ withブロックから抜ける時の処理 """
        print(f'exit: {exc_type}, {exc_value}, {traceback}')
        self.file.close()

if __name__ == '__main__':
    with MyFileReader('test.txt') as f:
        print(f.read())

中身を表示するテキストファイルも作成しておきましょう。

test.txt

このファイルはtest.txtです。

動かしてみます。

consoleで動かす

$ python sample2.py
init
enter
このファイルはtest.txtです。

exit: None, None, None

このサンプルの出力からもわかるように、with文に対応したMyFileReaderクラスの処理は、次のように動きます。

  • インスタンスのイニシャライズを行う(init)
  • 前処理を動かす(enter)
  • withブロック内の処理を実行する(f.read)
  • 後処理を実行する(exit)

ここで、__enter____exit__の役割をもう少し詳しくみてみます。

__enter__の役割

__enter__は、with文で利用されるオブジェクトの処理の前処理を行います。

with文にターゲット(... as f:fの部分)を指定した場合には__enter__メソッドの戻り値がそのターゲットに指定された変数に代入されて利用できます。

そのため、戻り値は、これが実装されたクラスのインスタンスを返すように設定することが多いです。

MyFileReaderでは開いたファイルオブジェクトを返しているので、withブロック内ではこのfに対してファイルの操作を実行していることになります。

__exit__の役割

__exit__では後処理の部分を担います。 withブロック内の処理を行った後にこちらのメソッドが呼び出され、withブロック内で発生した例外の処理もこちらに記述することができます。

__exit__には3つの引数を持たせることができます。ここにwithブロック内の例外のデータが入ってきますが、うまくいっていればすべてNoneが返ってきます。

  • exc_type: 例外の型
  • exc_value: 例外の値
  • traceback: 例外のtraceback

注意点として、__exit__メソッド内での例外は、withブロックの中身で発生した例外のみ扱えるので、たとえば、with文自体で実行している処理(open()メソッドなど)の処理のエラー処理は行いません。指定されたファイルが存在しないなどのエラーは__exit__に届く前に処理されます。

実際に__exit__メソッドでエラーを処理するパターンも確認してみます。

sample3.py

import traceback

class MyFileReader2:
    def __init__(self, filename):
        self.filename = filename
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, 'r')
        return self.file

    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            print('エラーが発生しました')
            for message in traceback.format_exception(exc_type, exc_value, tb):
                print(message)
        self.file.close()

if __name__ == '__main__':
    # read modeで開いているにも関わらず、書き込みをしようとしている
    with MyFileReader2('test.txt') as f:
        print(f.write('This is test.txt file.'))

consoleで動かす

$ python sample3.py
エラーが発生しました
Traceback (most recent call last):

  File "sample.py", line 55, in <module>
    print(f.write('This is test.txt file.'))

io.UnsupportedOperation: not writable

今回は、withブロック内で発生したio.UnsupportedOperationというエラーを拾って表示しています。 この場合にも、close()関数はちゃんとうごくようになっているため、ファイルの閉じ忘れによるバグやメモリリークなどが発生しないようにできています。