Python I/Oの再考(3): I/Oストリームのクラス階層

##### [前回] [Python I/Oの再考(2): ioモジュールの高レベルインターフェース](https://qiita.com/mingchun_zhao/items/3206250501d9c8ac69e3)

## はじめに

前回は、`io`モジュールの高レベルインターフェースを理解しました。
今回は、`io`モジュールのクラス群です。

## 抽象基底クラス(ABC, Abstract Base Class)とは

- インターフェースを定義する方法を提供
    - 具象クラスの実装に役立つデフォルトの実装を提供
- 仮想サブクラスを導入可能
    - 他のクラスの継承ではないが、`isinstance()`、`issubclass()`により認識可能

## `io`モジュールが提供する抽象基底クラス(ABC)と階層

- `IOBase`: I/O階層の最上位、全I/Oクラスのベースクラス
    - `RawIOBase`: IOBaseを拡張、Rawバイナリストリーム操作用
    - `BufferedIOBase`: IOBaseを拡張、バッファリングバイナリストリーム操作用
    - `TextIOBase`: IOBaseを拡張、テキストストリーム操作用

## 抽象基底クラスと具象クラスの関係

- 抽象基底クラスABC(ストリームのカテゴリ分類に使用)
    - 具象クラス1(標準のストリームを実装)
    - 具象クラス2(標準のストリームを実装)
    - ... ...

## I/Oストリームのクラス階層

- `IOBase`: ストリームの基本インターフェースを定義、デフォルトは読み込み/書き込み/シーク不能
    - `RawIOBase`: IOBaseを継承、Rawバイナリストリームで`bytes`の読み書きを行う
        - `FileIO`: RawIOBaseを継承、ファイルシステムのファイルへのインターフェースを提供
    - `BufferedIOBase`: IOBaseを継承、バッファリングバイナリストリームで、RawIOBaseのRawバイナリストリームへの高レベルアクセスを提供
        - `BufferedReader`: BufferedIOBaseを継承、読み込み可能かつシーク不能なRawバイナリストリームへのバッファリングインターフェースを提供
        - `BufferedWriter`: BufferedIOBaseを継承、書き込み可能かつシーク不能なRawバイナリストリームへのバッファリングインターフェースを提供
        - `BufferedRandom`: BufferedReaderとBufferedWriterを継承、シーク可能なRawバイナリストリームへのバッファリングインターフェースを提供
        - `BufferedRWPair`: BufferedIOBaseを継承、シーク不能なRawバイナリストリームへの二つのバッファリングインターフェースを提供(それぞれ読み込みと書き込み)
        - `BytesIO`: BufferedIOBaseを継承、バイナリストリームで、インメモリbytesバッファを使用
    - `TextIOBase`: IOBaseを継承、テキストストリームのベースクラス
        - `TextIOWrapper`: TextIOBaseを継承、テキストストリームでBufferedIOBaseのバッファリングバイナリストリームへの高レベルアクセスを提供
        - `StringIO`: TextIOBaseを継承、テキストストリームで、インメモリテキストバッファを使用

## `FileIO`クラスと`BufferedReader`クラスの違いを検証

`FileIO`は、`RawIOBase`(バッファリングなし)を継承し、
`BufferedReader`は、`BufferedIOBase`(バッファリングあり)を継承します。

何がどう違うか、AlmaLinux環境で検証してみます。

### FileIOクラスによるファイル読み込みを確認

- ファイル`test_big_file`を作成

ファイル中身は任意。
ファイルサイズは6020バイトとなっています。

```
$ ls -l test_big_file
-rw-rw-r-- 1 zhao zhao 6020  8月 21 11:42 test_big_file
```

- Pythonスクリプト作成
    - ファイルを、バッファリングを無効にし、バイナリモードで開く
    - 5120バイトを読み込む

```test_fileio.py
with open("test_big_file", "rb", buffering=0) as f:
    print(type(f))
    data = f.read(5120)
    print(type(data))
    print(len(data))
```

- Pythonスクリプトを実行
    - straceコマンドでシステムコールを採取

```shell
$ strace -f -o strace_fileio.log python test_fileio.py
```

- straceログ`strace_fileio.log`を開き
    - `test_big_file`ファイルに対するシステムコールを確認

```shell
openat(AT_FDCWD, "test_big_file", O_RDONLY|O_CLOEXEC) = 3
read(3, "\346\244\234\350\250\274\346"..., 5120) = 5120
```

`システムコールread()`は1回のみ実行される。
`ソースコードのf.read()メソッド`で指定した5120バイトを、
バイト単位でびったり読み込んでくれています。
FileIOクラスの読み込み単位は、1バイトであることがわかります。

### BufferedReaderクラスによるファイル読み込みを確認

- Pythonスクリプト作成
    - ファイルをバイナリモードで開く
    - 5120バイトを読み込む

```test_buffered-reader.py
with open("test_big_file", "rb") as f:
    print(type(f))
    data = f.read(5120)
    print(type(data))
    print(len(data))
```

- Pythonスクリプトを実行
    - straceコマンドでシステムコールを採取

```shell
$ strace -f -o strace_buffered-reader.log python test_buffered-reader.py
```

- straceログ`strace_buffered-reader.log`を開き
    - `test_big_file`ファイルに対するシステムコールを確認

```shell
openat(AT_FDCWD, "test_big_file", O_RDONLY|O_CLOEXEC) = 3
read(3, "\346\244\234\350\250\274\346"..., 4096) = 4096
read(3, "\350\250\274\346\244\234\350"..., 4096) = 1924
close(3)
```

`システムコールread()`が2回実行されました。
1回目は、4096バイト(バッファサイズ分)を読み込み、
2回目は、1924バイトを読み込んでいます。
合わせると、4096+1924=6020バイト
あれ、`ソースコードのf.read()メソッド`で指定した5120バイトではなく、
ファイルサイズ6020バイト分をすべて読み込んでいます。

つまり、BufferedReaderクラスの読み込み単位は、
バッファサイズ(4096バイト)であることがわかります。

## おわりに

I/Oストリームのクラス階層とそれぞれの違いを理解しました。
次回も続きます。お楽しみに。