ざっくりPythonのおさらい その4。contextlib系


前回に引き続いて、`contextlib`周りのおさらいです。
その他のおさらい事項にについては、以下ご参照ください。

[ざっくりPythonのおさらい その1。基本文法など]
https://qiita.com/48hands/items/31b7ac2b49addbb8658c

[ざっくりPythonのおさらい その2。おもにデータ保存]
https://qiita.com/48hands/items/ab86b7463268e669d216

[ざっくりPythonのおさらい その3。Numpy, Pandas]
https://qiita.com/48hands/items/b1a71000533e129b27f2


## contextlib.contextmanager

`contextlib.contextmanager`はデコレータに関連したライブラリ。
withステートメントを使って、簡単にデコレーションできるようになる。

まずはじめに、デコレータの復習。
以下がデコレーションの基本。デコレーション対象の関数`f`にデコレーションを施す関数を`@tag`で指定している。

```python
import contextlib


def tag(f):
    def _wrapper(content):
        print("decorated start")
        r = f(content)
        print("decorated end")
        return r

    return _wrapper


@tag
def f(content):
    print(content)


if __name__ == '__main__':
    f('this is test message')
```

実行結果は以下。

```
decorated start
this is test message
decorated end
```

ここからデコレーションを施す関数`tag`に引数を渡したい場合には、以下のように記述する。

```python
import contextlib


def tag(name):
    def _tag(f):
        def _wrapper(content):
            print("{} start".format(name))
            r = f(content)
            print('{} end'.format(name))
            return r

        return _wrapper

    return _tag


@tag('decorated')
def f(content):
    print(content)


if __name__ == '__main__':
    f('this is test message')
```


これをわかりやすくするために、`contextlib.contextmanager`を使う。`contextlib.contextmanager`で書くと以下のようになる。

`yield`を使ってデコレーション対象の関数`f`を呼び出している。


```python
@contextlib.contextmanager
def tag(name):
    print("{} start".format(name))
    yield
    print('{} end'.format(name))


@tag('decorated')
def f(content):
    print(content)


if __name__ == '__main__':
    f('this is test message')
```

さらに...
ここで、関数fを使っているが、`f`を使わずに、以下のように記述できる。

```python
import contextlib


@contextlib.contextmanager
def tag(name):
    print("{} start".format(name))
    yield
    print('{} end'.format(name))


if __name__ == '__main__':
    with tag('decorated'):
        print('this is test message')
```

`contextlib.manager`はデコレータとしての役割をwithステートメントを使って果たすようなことができている。
とてもわかりやすい！


さらに、withステートメントをネストするようなことも簡単にできる。

```python
import contextlib


@contextlib.contextmanager
def tag(name):
    print("{} start".format(name))
    yield
    print('{} end'.format(name))


if __name__ == '__main__':
    with tag('decorated'):
        print('this is test message')
        with tag('decorated2'):
            print('this is nested message')
```

実行すると以下のようになる。

```
decorated start
this is test message
decorated2 start
this is nested message
decorated2 end
decorated end
```

## contextlib.ContextDecorator

用途は`contextlib.contextmanager`と似ている気がするが、実装方法が異なる。`ContextDecorator`を継承したクラスを定義して利用する。

```python
import contextlib


# contextlib.ContextDecoratorを継承したクラスを定義
class tag(contextlib.ContextDecorator):
    def __init__(self, name):
        self.name = name
        self.start_tag = '<{}>'.format(name)
        self.end_tag = '</{}>'.format(name)

    # デコレーションに入ったときの動作を定義
    def __enter__(self):
        print(self.start_tag)

    # デコレーションを抜けるときの動作を定義
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.end_tag)


if __name__ == '__main__':
    with tag('decorated'):
        print('this is test message')
        with tag('decorated2'):
            print('this is nested message')
```

```
<decorated>
this is test message
<decorated2>
this is nested message
</decorated2>
</decorated>
```


`def __exit__(self, exc_type, exc_val, exc_tb):`の`exec_type`, `exec_val`, `exec_tb`に何が入っているか確認してみる。

```python
import contextlib


# contextlib.ContextDecoratorを継承したクラスを定義
class tag(contextlib.ContextDecorator):
    def __init__(self, name):
        self.name = name
        self.start_tag = '<{}>'.format(name)
        self.end_tag = '</{}>'.format(name)

    # デコレーションに入ったときの動作を定義
    def __enter__(self):
        print(self.start_tag)

    # デコレーションを抜けるときの動作を定義
    # Exceptionなどの情報が引数に入っている。
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(self.end_tag)
        print('Exception type: {}'.format(exc_type))
        print('Exception value: {}'.format(exc_val))
        print('Exception trace back {}'.format(exc_tb))


if __name__ == '__main__':
    with tag('decorated'):
        raise Exception('Error!!')
        print('hoge hoge')
```

```
<decorated>
Traceback (most recent call last):
</decorated>
Exception type: <class 'Exception'>
Exception value: Error!!
Exception trace back <traceback object at 0x109d01f88>
  File "/Users/nagakuray/Desktop/TDD-practice/lesson/context.py", line 26, in <module>
    raise Exception('Error!!')
Exception: Error!!
```

## contextlib.supress

存在しないファイル`/path/to/somefile.tmp`を例として、
以下のようなコードで例外`FileNotFoundError`を抑圧したいような場合。

```python
import os

try:
    os.remove('/path/to/somefile.tmp')
except FileNotFoundError:
    pass
```

上記のコードは、`contextlib,suppress`を使ってwithステートメントで以下のように抑圧(suppress)できる。

```python
import contextlib
import os

with contextlib.suppress(FileNotFoundError):
    os.remove('/path/to/somefile.tmp')
```

## 標準入力、標準出力、標準エラー出力、出力のリダイレクト

### 標準入力

キーボードの入力を待ち受けて出力する。

```python
import sys

for line in sys.stdin:
    print(line)
```

ループして標準入力を待受て出力する場合はこちら。

```python
import sys

for line in sys.stdin:
    print(line)
```

### 標準出力

```python
print('hello')
```

これを以下のように書ける。

```python
import sys


sys.stdout.write('hello')
```

### 標準エラー出力

```python
import sys
import logging

logging.error('Error!')
sys.stderr.write('Error!!')
```

### 出力のリダイレクト

`contextlib.redirect_stdout`で標準出力をリダイレクトしてファイルに書き出しもできる。Python3.5からの機能。

```python
import contextlib

with open('stdout.log', 'w') as f:
    with contextlib.redirect_stdout(f):
        print('hello')
```

標準エラー出力をリダイレクトしてファイルに書き出ししたい場合は、`contextlib.redirect_stderr`を使う。

```python
import contextlib

with open('stderr.log', 'w') as f:
    with contextlib.redirect_stderr(f):
        print('hello')
```

## contextlib.ExitStack

ExitStackを使うと、
callbackが明示的に記述できて、必ず実行したい処理が一目見てわかる。

以下のような処理が合った場合を考える。

```python
def is_ok_job():
    try:
        print('do something')
        raise Exception('Error!')
        return True
    except Exception:
        return False


def cleanup():
    print('clean up')


try:
    is_ok = is_ok_job()
    print('more task')
finally:
    # is_okがTrueでなかった場合にcleanupの処理を実行する
    if not is_ok:
        cleanup()
```

実行結果は以下。

```
do something
more task
clean up
```

この`try~finally`を置き換えるのが、`contextlib.ExitStack`
以下のように使う。

```python
import contextlib

def is_ok_job():
    try:
        print('do something')
        raise Exception('Error!')
        return True
    except Exception:
        return False


def cleanup():
    print('clean up')


with contextlib.ExitStack() as stack:
    # cleanupをスタックしておく。
    stack.callback(cleanup)

    is_ok = is_ok_job()
    print('more more more')

    # is_okがTrueの場合にpop_allでスタックを空にする。
    # もしFalseの場合はstackされたcleanupがコールバックされる。
    if is_ok:
        stack.pop_all()
```

実行結果は以下。

```
do something
more more more
clean up
```

cleanup関数が引数を持っていて引数を渡す場合は、以下のように書ける。

```python
import contextlib

def is_ok_job():
    try:
        print('do something')
        raise Exception('Error!')
        return True
    except Exception:
        return False


def cleanup(msg, *args, **kwargs):
    print(msg)
    print(args)
    print(kwargs)


with contextlib.ExitStack() as stack:
    # 引数を渡す場合は、以下のように渡す。
    stack.callback(cleanup, 'hello', 'good', 'ng', mysql='3306',
                   postgres='5432')

    is_ok = is_ok_job()
    print('more more more')

    if is_ok:
        stack.pop_all()
```

実行結果は以下。

```
do something
more more more
hello
('good', 'ng')
{'mysql': '3306', 'postgres': '5432'}
```

また、withステートメントの中でインナー関数として`cleanup`関数を記述できる。
`@stack.callback`をインナー関数に記述する。

```python
with contextlib.ExitStack() as stack:
    @stack.callback
    def inner_cleanup():
        print('inner cleanup')


    is_ok = is_ok_job()
    print('more more more')

    if is_ok:
        stack.pop_all()
```

実行結果は以下。

```
do something
more more more
inner cleanup
```

callbackが明示的に記述されることによって、必ず実行したい処理が一目見てわかるのがExitStackの利点といえる。