SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

CodeKataで遊ぶ Kata04:データマンジング

CodeKataやってみた記事、第四弾です。

今回は、Kata04: Data Mungingをやっていきます。

codekata.com

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記事、若干ですがアクセスがありました。ありがとうございます。

*1:リファクタリングオーム社)』等の著者

*2:イナリサーチを、異なる方法で五回記述する問題。過去記事で解いているので、良ければ参照ください