とりゅふの森

GCPデータエンジニアとして生きる

【Python入門】with構文を使いこなそう

f:id:true-fly:20210730000421p:plain

こんにちは、とりゅふです。みなさん、Pythonでファイルを読み込みする時って普段どのように書きますか?私ならこんな感じで書きます。

with open('text.csv') as f:
    text = f.read()

print(text)

withを使ってファイルを開き、中のテキストを取り出し、printするプログラムです。では、このwith、何者か説明できますか?
今回はこのwith構文についてまとめました。
本記事のコードはすべて、Python 3.7.6で実行しています。

入門 Python 3 第2版

入門 Python 3 第2版

Amazon

with構文の正体

まずこのwith構文の正体について見ていきましょう。with前処理と後処理を実行してくれる構文です。
前処理と後処理が必要な処理って必ずありますよね。例えばファイルのOpen、Closeだったり、データベースの接続、切断だったり、これらは主処理の前後に前処理と後処理が実行される処理になります。
例えば、with open('text.csv')のように、ファイルの読み込みでwithを使った場合、内部的には以下のような処理になります。 - ファイルをopenする - ファイルの読み書きなどの主処理を実行する - ファイルをcloseする

ではこれをwithなしで書くとどうなるでしょうか?

f = open('text.csv')
txt = f.read()
print(txt)
f.close()

open()してread()するまでは良いのですが、close()するの忘れそうになりませんか?ファイルをcloseし忘れて、他の処理がファイルにアクセスできなくなったり、無駄なリソースを使い続けたままになったりと、バグの元になりがちですよね。
with構文を使えば、このopen()close()を意識することなく処理が書けるので、より品質の高いコードを書くことができるのです。

withを使えるクラス

with構文で呼び出しができるクラスは、__enter__()と、__exit__()を実装したクラスになります。例えば以下のようなクラスです。

class WithSample:
    def __init__(self):
        print('初期化')

    def __enter__(self):
        print('前処理')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('後処理')

    def execute(self):
        print('主処理')


if __name__ == '__main__':
    with WithSample() as w:
        w.execute()
初期化
前処理
主処理
後処理

まずはまずは通常のクラスのように、__init__()が実行され、その次に前処理として__enter__()が実行されます。ここでreturnしたオブジェクトが、with構文のasに当たるオブジェクトになります。ここではwith構文内でWithSampleクラスの処理を実行したいので、selfを返すようにしています。
withブロック内で主処理を実行し、withブロックから抜ける時に、後処理として__exit__()が実行されます。
ちなみにですが、__enter__()と、__exit__()のいずれかが実装されていないクラスをwith構文で呼び出そうとすると、AttributeErrorとなります。出入口は必須ってことですね。

with構文の例外処理

次は例外発生時の検証として、以下のようなわり算だけができる電卓クラスを作り、わざとZeroDevisionErrorを発生させてみます。

class Calculator:

    def __enter__(self):
        print('計算を開始します。')
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print('計算を終了します。')

    def devide(self, n, m):
        print('{} ÷ {} ='.format(n, m), end='')
        try:
            print(n / m)
        except Exception as e:
            print('[ERROR]')
            raise e


if __name__ == '__main__':

    print('** [START] 1回目の計算機 **')
    with Calculator() as calc1:
        calc1.devide(10, 5)
        calc1.devide(10, 2)
    print('** [END] 1回目の計算機 **\n')

    print('** [START] 2回目の計算機 **')
    with Calculator() as calc2:
        calc2.devide(10, 4)
        calc2.devide(10, 0)
        calc2.devide(10, 3)
    print('** [END] 2回目の計算機 **\n')
** [START] 1回目の計算機 **
計算を開始します。
10 ÷ 5 =2.0
10 ÷ 2 =5.0
計算を終了します。
** [END] 1回目の計算機 **

** [START] 2回目の計算機 **
計算を開始します。
10 ÷ 4 =2.5
10 ÷ 0 =[ERROR]
計算を終了します。
Traceback (most recent call last):
*** 中略 ***
ZeroDivisionError: division by zero

1回目の計算機は最後まで正常に処理され、2回目の計算機は、途中でZeroDivisionErrorが発生していますが、ちゃんと__exit__()の処理は実行されていることがわかります。
with構文を使えば、処理で例外が発生しても、try except finallyなどを用いずに、後処理を実行してくれます。ファイルをOpenして、その間に例外が発生しても、ちゃんとcloseはされます。

まとめ

以上、今回はPythonのwith構文についてまとめました。自分で定義したクラスで、前処理、後処理が必要な処理については、__enter__()と、__exit__()を実装し、with構文で呼び出しができるようにすれば、呼び出しが簡単で、品質の高いプログラムにつながると思います。