CodeKataやってみた記事、第四弾です。
今回は、Kata04: Data Munging
をやっていきます。
Data Munging
とは、
受け取ったデータのフォーマットをその他のフォーマットに変換する技法全般
のことを指すそうです。初耳です。(リンク)
問題概要
Martin Fowler*1は、Kata02*2で苦しめられたとのこと。
理由は、「単機能で学術的」であることから。
それはその通りなので、趣向を変えた三問を用意。先読みせずに、一問ずつ答えましょう。
…という感じです。
なお、いずれの問題でも、とあるファイルを読み込んで利用します。元ファイルはKata04
のサイトから各自取得してください。
問1:天気データ
2002年6月のモリスタウン気象情報のテキストファイルを読み込み、最高気温と最低気温の差が最も小さい日を出力。
1列目:日付 2列目:最高気温 3列目:最低気温
問2:サッカーの試合
2001年2月のイギリスプレミアリーグの結果のテキストファイルを読み込む。
「F(for)」の列には自身が取ったゴール数、「A(against)」の列には取られたゴール数が格納されている。
この差が一番小さいチームの、チーム名を表示する。
問3:DRY原則の適用(融合)
できる限り、二つのプログラムの重複を排除し、共通機能を作る。
二つのプログラムと、共通機能を作成する。
Kataとしての問
元のプログラムを書くときにあなたが作った設計上の決定は、共通コードを除外することを容易にしたり、難しくしたりしましたか?
二番目のプログラムは、最初のプログラムの影響を受けましたか?
共通部分にプログラムを分解するのは、常に良いことですか?この要件のためにプログラムの可読性が損なわれましたか? メンテナンス性はどうですか?
いつものように、続きは一応隠します。(直接見ると隠れません)
自分なりの答え
問1:天気データ
- weather.py
import numpy as np import sys def main(): # last row is total. val = np.loadtxt("weather.dat", dtype=str, skiprows=1, usecols=(0, 1, 2))[:-1] minimum = sys.maxsize day = '' for v in val: day_value = int(v[1].replace('*', '')) - int(v[2].replace('*', '')) if day_value < minimum: minimum = day_value day = v[0] print(day) if __name__ == '__main__': main()
テキストを扱うのが面倒で、横着してnumpy
を使っています。
また、最高気温、最低気温共に、その月の最も大きい値に32*
のように*
が付いているため、それを消す処理を追加し、差を算出しています。
ロジック自体は、どうということはないと思います。
問2:サッカーの試合
差が一番小さい
に関して、差の絶対値が最も小さいと解釈して、解いています。
- football.py
import re import sys def main(): val = [] minimum = sys.maxsize team = '' for line in open("football.dat"): values = re.sub(' +', ' ', line.strip()).split(' ') if len(values) != 10: continue val.append((values[1], values[6], values[8])) for v in val: score = abs(int(v[1]) - int(v[2])) if score < minimum: minimum = score team = v[0] print(team) if __name__ == '__main__': main()
ファイル内に、複数の半角スペースが全く登場しない行があるため、numpy
ではうまく取り込めませんでした。
そこで、ファイル内の半角スペースを全て単一の半角スペースに置換。半角スペースで区切った項目数が10の行のみ取り込むこととしました。
あとはまあ、FとAについて差の絶対値を取り、最も小さいものを保持して出力するだけです。
問3:DRY原則の適用(融合)
さて、上記二つのプログラムに共通する部分は、最低値の判定と保持です。
というわけで、そこだけ切り出したクラスを作成します。
- mincalcs.py
import sys class min_calculator(object): """最低値計算クラス""" def __init__(self): self._min = sys.maxsize self._name = '' def add(self, value: int, name: str): """値の追加""" if self._min > value: self._min = value self._name = name def name(self): """最低値の名称""" return self._name
add
というメソッド名に問題はあると思いますが、判定値と名称を受け取って、クラスが現在保持する最低値と比較。
受け取った値のほうが小さければ、その名称を保持するクラスです。
最低値自体を公開していないのは、問題に要求されていないから、です。
これを利用して、上記2クラスを書き直します。
- weather.py
import numpy as np from mincalcs import min_calculator def main(): # last row is total. val = np.loadtxt("weather.dat", dtype=str, skiprows=1, usecols=(0, 1, 2))[:-1] mins = min_calculator() for v in val: mins.add(int(v[1].replace('*', '')) - int(v[2].replace('*', '')), v[0]) print(mins.name()) if __name__ == '__main__': main()
- football.py
import re from mincalcs import min_calculator def main(): val = [] for line in open("football.dat"): values = re.sub(' +', ' ', line.strip()).split(' ') if len(values) != 10: continue val.append((values[1], values[6], values[8])) mins = min_calculator() for v in val: mins.add(abs(int(v[1]) - int(v[2])), v[0]) print(mins.name()) if __name__ == '__main__': main()
ファイルの解釈はそれぞれ異なるので、そこはバラバラにしつつ、判定のみmin_calculator
で行っています。
両者とも、各問で書いたソースより短くなっています。
まだまだ綺麗にする方法はありますが、ほどほどにしています。
Kataとしての問
- 元のプログラムを書くときにあなたが作った設計上の決定は、共通コードを除外することを容易にしたり、難しくしたりしましたか?
最低値の判断は、ありふれたロジックなので、そこは当たり前に記述しました。
強いて言うなら、それが共通コードにする場合に楽でした。
短いプログラムなので、分割はほとんど考慮しませんでしたが、記述上は値の取り出しと判断を分けていたため、それは良かったと思います。
- 二番目のプログラムは、最初のプログラムの影響を受けましたか?
受けています。先の問題を見るな、という制約があったので、最低値判断部分はほとんど同じ仕組みにしていました。
- 共通部分にプログラムを分解するのは、常に良いことですか?この要件のためにプログラムの可読性が損なわれましたか? メンテナンス性はどうですか?
共通部分にプログラムを分解するのは、常に良いことだと思います。ただし、共通部分をどこに置くかという、別の問題は発生します。
可読性はむしろ向上しており、メンテナンス性も向上した、と私は思います。
解いた感想
プログラムの共通化という意味では、良い練習だと思いました。
なお、Pythonでテキストファイルを扱うのが初めてだったので、個人的にはそちらのほうが勉強になりました。
numpy
、便利ですね。
おわりに
Kata02
に比べたら、解いていて楽しかったです。02は思いつくまでが地獄でした…
あと、過去のCodeKata
記事、若干ですがアクセスがありました。ありがとうございます。