はじめに
ヨーソロー!1 nikkieです。
ここ数日はcsvモジュールで知ったことをアウトプットしています。
今回はcsv.DictReader
のソースコードリーディングで知ったことです。
目次
csv.DictReader
とは
CSV形式のファイルをPythonで読み書きするときに使うのが、標準ライブラリのcsv
。
以下のようなコードで読み込めます(この記事の対話例は Python 3.10.9 です)。
some.csv
name,price apple,1000 banana,500
>>> import csv >>> with open("some.csv", newline="", encoding="utf8") as f: ... reader = csv.reader(f) ... for row in reader: ... print(row) ... ['name', 'price'] ['apple', '1000'] ['banana', '500']
csv.reader
では各行がリストになります。
整数インデックスで各列の値にアクセスします。
整数インデックスだとマジックナンバーみがありますよね。
csv
モジュールにはcsv.DictReader
というクラスもあります!
https://docs.python.org/ja/3/library/csv.html#csv.DictReader
このクラスを使うと、CSVの各行を辞書で扱えます。
整数インデックスからヘッダーの項目名に変わる2ので、各列の値へのアクセスがより分かりやすいと思います!
>>> with open("some.csv", newline="", encoding="utf8") as f: ... reader = csv.DictReader(f) ... for row in reader: ... print(row) ... {'name': 'apple', 'price': '1000'} {'name': 'banana', 'price': '500'}
DictReader
にもreader
同様、dialect
引数があります3。
csv.DictReader
のソースコードリーディング
ソースコードはそんなに長くありません4。
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L80-L127
csv.DictReader
インスタンスはイテレータ
上で示した対話例ではfor
文で使用しました。
for
文で使えるのでイテラブルです
実装を見ると、特殊メソッド__iter__
と__next__
を持つので、イテレータと分かります。
__iter__
: https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L90-L91- インスタンス自身を返しています
__next__
: https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L107-L127- CSVファイルの1行1行を返す処理です(この後見ます)
class DictReader: # 省略 def __iter__(self): return self def __next__(self): # 省略
イテレータはイテラブルです
csv.DictReader
はcsv.reader
に委譲している
「継承なのかな」と想像していたため、実装を見て驚きがありました。
読んでいくと、この委譲、うまくできていると思うんです!
__init__
でcsv.reader
インスタンスを初期化し、reader属性に持つ- https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L86
- 初期化時にファイルオブジェクトやdialectを渡している
class DictReader: def __init__(self, f, fieldnames=None, restkey=None, restval=None, dialect="excel", *args, **kwds): # 一部抜粋 self._fieldnames = fieldnames # list of keys for the dict self.reader = reader(f, dialect, *args, **kwds) self.line_num = 0
__next__
の実装です
class DictReader: def __next__(self): # 一部抜粋 row = next(self.reader) d = dict(zip(self.fieldnames, row)) lf = len(self.fieldnames) lr = len(row) if lf < lr: d[self.restkey] = row[lf:] elif lf > lr: for key in self.fieldnames[lr:]: d[key] = self.restval return d
next(self.reader)
とcsv.reader
に1行処理させる- https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L111
- なので、ここで取れるのはリスト(整数インデックスでアクセスする行)
csv.reader
が処理した1行とfieldnames
から辞書を作る- https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L119
fieldnames
が辞書のキー、csv.reader
が処理した1行が辞書の値
fieldnames
と1行の長さが合わない場合の処理- ドキュメントより「列が fieldnames より多くのフィールドを持っていた場合、残りのデータはリストに入れられて、 restkey により指定されたフィールド名 (デフォルトでは None) で保存されます。」
- https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L123
- fieldnamesの長さを上回った行の要素を、
restkey
というキーに持たせている - なので、
len(fieldnames)+1
個のキーと値を持つ
- ドキュメントより「非ブランクの列が fieldnames よりも少ないフィールドしか持たない場合、不明の値は restval の値 (デフォルトは None ) によって埋められます。」
- https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L125-L126
- 行よりもfieldnamesが長い場合は、
restval
を値としてキーを持たせている - なので、
len(fieldnames)
個のキーと値を持つように補われる
- ドキュメントより「列が fieldnames より多くのフィールドを持っていた場合、残りのデータはリストに入れられて、 restkey により指定されたフィールド名 (デフォルトでは None) で保存されます。」
ドキュメントで記載された動きがコードで表現されていて、うまくできているな〜と舌を巻きました
fieldnames
属性
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L93-L101
class DictReader: @property def fieldnames(self): if self._fieldnames is None: try: self._fieldnames = next(self.reader) except StopIteration: pass self.line_num = self.reader.line_num return self._fieldnames
@property
を使った実装です- 省略しましたが、setterも定義されています
_fieldnames
属性がすでに設定されていたらそれを返します__init__
で渡されたらすでに設定されますfieldnames
属性にすでにアクセスがあったら設定されています
_fieldnames
属性が設定されていない(値がNone
)のときnext(self.reader)
で返した行を設定します- 1行目について呼ばれるように実装しています(後述)
- 1行目がヘッダー行ならは、ヘッダーが
fieldnames
となりますね
- 1行目がヘッダー行ならは、ヘッダーが
「1行目について呼ばれる」という点ですが、__next__
の中に1行も処理していなかった(line_num
属性の値が0
5だった)ら、self.fieldnames
と呼び出しています。
https://github.com/python/cpython/blob/v3.11.3/Lib/csv.py#L108-L110
返ってきたfieldnamesを使うわけではなく、_fieldnames
属性の設定が目的の呼び出しと理解しました(副作用のために使われる というコメントもあります)。
def __next__(self): if self.line_num == 0: # Used only for its side effect. self.fieldnames row = next(self.reader) # 省略
ミノ駆動本より 継承より委譲
8章「密結合」の「8.2 密結合の各種事例と対処方法」で、良い構造にするために「継承より委譲」が紹介されていたのを思い出しました(8.2.1)。
csv.reader
を継承してcsv.DictReader
クラスを定義するのではなく、csv.reader
インスタンスを属性に持つcsv.DictReader
クラスは、__iter__
と__next__
メソッドでイテレータにし、csv.reader
クラスと振る舞いを揃える- 揃っているから私は継承による実装なのかなと思ったわけですね
__next__
の実装は、csv.reader
が返した行(リスト)を別のリストfieldnames
と組合せて辞書を作るというものでした- 実装としてきれいだなーと思います(委譲の一例なのかなと認識しました)
終わりに
csv.DictReader
のソースコードリーディングで知ったことをアウトプットしました。
csv.DictReader
クラスはcsv.reader
インスタンスを持つ(委譲)- CSVファイルの1行を辞書として返すのに
csv.reader
の処理結果を加工する
- CSVファイルの1行を辞書として返すのに
fieldnames
属性、CSVファイルの1行目(ヘッダー想定)が設定されるように__next__
など工夫している
比較的短いクラスでしたし、実装に込み入ったところはなく、すっと読めていくつか気づきもありました🙌
csv.reader
クラスの中も覗いてみたいところですが、_csv
に実装があるようで、Python実装ではない実装がどこかにありそうだな〜くらいの認識です(探すところからですね。C実装だと思うので、いまはまだ私は読めないでしょう)
-
渡辺曜さん、お誕生日おめでとうございます!
↩はい、みなさんお祭りです!!
— 斉藤 朱夏⛵️曜ちゃんHBD⛵️ (@Saito_Shuka) 2023年4月16日
我が渡辺曜ちゃんの誕生日です!!!
今日はハンバーグを食べましょう!! https://t.co/4wJ8pNszAI - 辞書のキー名は与えることもできます。ドキュメントより「fieldnames が省略された場合、ファイル f の最初の列の値が fieldnames として使われます。」↩
- 詳しくは ↩
-
ここ数日のcsvモジュール関連のアウトプットでソースコードに入り浸ったのですが、
DictReader
はそんなに長いコードでないことに気づき、ちょっと読んでみました↩ -
__init__
でline_numを0に初期化しています↩