言語処理100本ノックをやっていたところPythonでのテキストオブジェクトの取り扱いで疑問が出てきたので色々検索してみたりPythonのドキュメントを見たりしてなんとか理解をすることができました。
一応共有しておきます。
結論は「ファイルオブジェクトの最終的な抽象基底クラスであるio.IOBaseがイテレータープロトコルをサポートしているから、テキストオブジェクトはイテレーターになる」です。
下記の記事ではこの話が気になった理由、調べた記事、参考にしたドキュメントの記載を自分のログとして残しています。
環境
macOS High Sierra 10.13.6
Python 3.7.2
疑問の生まれた原因
言語処理100本ノックの12問の話です。
「hightemp.txt」という日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルを使用します。
12. 1列目をcol1.txtに,2列目をcol2.txtに保存 各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
という問題です。
Pythonの理解もままならない僕はコレを自力で考えても無理なので参考になる他の回答者の回答を検索。
この記事を参考に考えることにしました。回答は以下。
with open("hightemp.txt") as f1, open("col1.txt","w") as f2, open("col1.txt","w") as f3:
cols=list(zip(\*\[row.split("\\t") for row in f1\]))
f2.write("\\n".join(cols\[0\]))
f3.write("\\n".join(cols\[1\]))
処理としてはwithブロックを使用して,hightemp.txtをopen()関数で帰ってきたテキストオブジェクトf1として扱います。col1.txtとcol2.txtも’w’パラメーターをつけて書き込みモード開き同様にf2,f3として扱います。
次にリスト内包表記を使用し、f1をforループで回し、split()関数で”\t”パラメーターつまりタブで分割したリストを*で展開し、zip()関数に渡したものをlist()関数でリストオブジェクトにしてからcolsに代入しています。
最後にf2,f3それぞれにcolsのindex番号0または1のリストをjoin()関数で改行をつけて書き込むことで回答としています。
なぜテキストオブジェクトをfor文で回せるのか?
2行目でテキストオブジェクトをforループ回しているけど、なんで?
for文のinに使えるものをiterable objectと呼ぶことは検索をしてわかりました。
反復可能オブジェクトの例には、(list, str, tuple といった) 全てのシーケンス型や、 dict や ファイルオブジェクト といった幾つかの非シーケンス型、 あるいは Sequence 意味論を実装した __iter__() メソッドか __getitem__() メソッドを持つ任意のクラスのインスタンスが含まれます。
と書いてあります。上記でのf1はファイルオブジェクトなのでiterableなことはわかりました。一応、dir()関数で__iter__()メソッドを持っているかどうかを確かめました。
>>> with open(‘hightemp.txt’) as f1: … dir(f1) … [’_CHUNK_SIZE’, ’__class__’, ’__del__’, ’__delattr__’, ’__dict__’, ’__dir__’, ’__doc__’, ’__enter__’, ’__eq__’, ’__exit__’, ’__format__’, ’__ge__’, ’__getattribute__’, ’__getstate__’, ’__gt__’, ’__hash__’, ’__init__’, ’__init_subclass__’, ’__iter__’, ’__le__’, ’__lt__’, ’__ne__’, ’__new__’, ’__next__’, ’__reduce__’, ’__reduce_ex__’, ’__repr__’, ’__setattr__’, ’__sizeof__’, ’__str__’, ’__subclasshook__’, ’_checkClosed’, ’_checkReadable’, ’_checkSeekable’, ’_checkWritable’, ’_finalizing’, ‘buffer’, ‘close’, ‘closed’, ‘detach’, ‘encoding’, ‘errors’, ‘fileno’, ‘flush’, ‘isatty’, ‘line_buffering’, ‘mode’, ‘name’, ‘newlines’, ‘read’, ‘readable’, ‘readline’, ‘readlines’, ‘reconfigure’, ‘seek’, ‘seekable’, ‘tell’, ‘truncate’, ‘writable’, ‘write’, ‘write_through’, ‘writelines’]
’__iter__‘を持っているのでイテラブルオブジェクトであることは間違いなさそうです。
テキストオブジェクトのf1がイテラブルならば、どこを区切りとしているの?
ということで再度Pythonのドキュメントを調べました。
open() 関数が返す file object の型はモードに依存します。 open() をファイルをテキストモード (‘w’, ‘r’, ‘wt’, ‘rt’, など) で開くのに使ったときは io.TextIOBase (特に io.TextIOWrapper) のサブクラスを返します。
今回はデフォルト(‘r’モード)で開いているのでf1の型はio.TextIOWrapperのサブクラスであることがわかりました。
io.TextIOWrapper、io.TextIOBaseの項目をよく見ても答えになりそうなものは乗っていませんでした。一応io.TextIOWrapperの項目に会ったnewlineパラメーターについての説明は後々参考になるので残しておきます。
newline は行末をどのように処理するかを制御します 。None, ”, ’\n’, ’\r’, ’\r\n’ のいずれかです。これは以下のように動作します:
ストリームからの入力を読み込んでいる時、newline が None の場合、universal newlines モードが有効になります。入力中の行は ’\n’、’\r’、または ’\r\n’ で終わり、呼び出し元に返される前に ’\n’ に変換されます。
さらに継承元をたどって、io.IOBaseというすべての I/O クラスの抽象基底クラスに辿り着いたところ、readline()メソッドの項目にまさにドンピシャの解説が書いてありました。
readlines(hint=-1) ストリームから行のリストを読み込んで返します。 hint を指定することで、読み込む行数を制御できます。もし読み込んだすべての行のサイズ (バイト数、もしくは文字数) が hint の値を超えた場合、読み込みをそこで終了します。
ただし、 file.readlines() を呼びださなくても for line in file: … のように file オブジェクトを直接イテレートすることができます。
io.IOBaseの説明にもいい感じの記載がありました。
IOBase (とそのサブクラス) はイテレータプロトコルをサポートします。 IOBase オブジェクトをイテレートすると、ストリーム内の行が yield されます。ストリーム内の行の定義は、そのストリームが (バイト列を yield する) バイナリストリームか (文字列を yield する) テキストストリームかによって、 少し異なります。下の readline() を参照してください。
コレです…これが僕の欲しかった情報です…!!!
結論
結論としてはopen()関数で返ってくるファイルオブジェクトは抽象基底クラスであるio.IOBaseがイテレータープロトコルをサポートしているのでイテラブルオブジェクトとして扱うことができる。
また,readline()メソッドのnewline()因数によって何を区切り文字とするかが決まる(デフォルトはuniversal newlines)
ということでした。