SE(たぶん)の雑感記

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

『テスト駆動開発』のソースをPythonで書いてみる(第Ⅰ部終了)

前回記事の続きです。

hiroronn.hatenablog.jp

テスト駆動開発』の第Ⅰ章を、Pythonで一通り流したので、感想とか書いてみます。

GitHubのページも貼っておきます。

github.com

GitHubのほうは、各章ごとにフォルダを分けており、それぞれREADMEで説明とか感想とか入れています。

ここでは、JavaPythonの違い*1について述べてみます。

言いたいこと

下は、説明がすごく長いので、先にもってきています。

言いたいことは一つ。

Pythonでも、簡単にテスト駆動ができる

ということです。
組んでいるとき、あまり迷わなかったです。コーディングに使っているVSCodeの優秀さもありますが。

型指定

Pythonは、メソッド引数や変数に、構文上型指定が不要です。
しかし、Python3.5からアノテーションが導入され、型指定は可能になりました。
今回は、型アノテーションを使ってみる目的で、途中から型指定はすべて入れるようにしています。

  • メソッド引数
def times(self, multiplier: int) -> Money:
    """通貨変換"""
    return Money(self._amount * multiplier, self.currency)

上のように書いた場合、multiplier: int: intは、構文チェック上無視されます。
同様に、-> Moneyも無視されます。

エディタ上は型を反映してくれます。

f:id:hiroronn:20171203151614p:plain

隠れていますが、上のtimesメソッドです。

相互参照

今回、最も頭を抱えた部分です。
本のサンプルでは、同一パッケージのクラスを相互参照しています。
Pythonでは、これをやられると型指定で困ります。
また、そもそも、自身を生成して返す場合も困ります。

  • dollar.py
    moneyクラスを継承しています。
"""dollar通貨"""
from .money import Money

class Dollar(Money):
    """ドル通貨を表します。"""
    def times(self, multiplier: int) -> Money:
        """通貨変換"""
        return Dollar(self._amount * multiplier)
  • money.py
    dollarの継承元。生成メソッドを持っています。*2
"""通貨基底クラス"""
from .dollar import Dollar

class Money(object):
    """通貨の継承元"""
    def __init__(self, amount: int) -> None:
        """初期化"""
        self._amount = amount

    def __eq__(self, other: "Money") -> bool:
        """override eq"""
        return (self._amount == other._amount) and \
            (self.__class__.__name__ == other.__class__.__name__)
    
    @staticmethod
    def dollar(amount: int) -> Dollar:
        """ドルを作成して、返す"""
        return Dollar(amount)
  • エラー内容
  File "C:\dev\python\tdd\tests\moneytests.py", line 3, in <module>
    from moneys.dollar import Dollar
  File "C:\dev\python\tdd\moneys\dollar.py", line 2, in <module>
    from .money import Money
  File "C:\dev\python\tdd\moneys\money.py", line 2, in <module>
    from .dollar import Dollar
ImportError: cannot import name 'Dollar'

こういう場合は、

等の対策があります。

継承、インターフェイス

Pythonにも、継承は存在します。
しかし、インターフェイスは存在しません。*3

そこで、代用としてABCMetaというクラスを使います。
これは、

  • 継承先で実装を強制できる

効果を持ち、実質的なインターフェイスとして利用できます。*4

Pythonメタクラス等に関しては、もうちょっと勉強が必要です…
とりあえず、インターフェイスの代用として使うには十分でした。
こんな感じで定義します。

"""式を表す"""
from abc import ABCMeta, abstractmethod
from .exchanger import CurrencyExchanger

class Expression(metaclass=ABCMeta):
    """式(演算)を表します。"""

    @abstractmethod
    def plus(self, addend: "Expression") -> "Expression":
        """加算"""
        pass

    @abstractmethod
    def reduce(self, bank: CurrencyExchanger, currency: str) -> "Money":
        """式を単純な形に変形する"""
        pass

    @abstractmethod
    def times(self, multiplier: int) -> "Expression":
        """指定倍"""
        pass

メタクラスとしてABCMetaを指定しています。そして、各メソッドには@abstractmethodを指定し、抽象メソッドであることを示しています。

@abstractmethodのメソッドを継承先で実装していないと、

..E.
======================================================================
ERROR: test_franc_multiplication (tests.moneytests.MoneyTest)
フランの計算
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\dev\python\tdd\tests\moneytests.py", line 24, in test_franc_multiplication
    self.assertEqual(Money.franc(10), five.times(2), "f10 == f10")
  File "C:\dev\python\tdd\moneys\franc.py", line 13, in times
    return Money(self._amount * multiplier)
TypeError: Can't instantiate abstract class Money with abstract methods times

----------------------------------------------------------------------
Ran 4 tests in 0.008s

FAILED (errors=1)

というように、Can't instantiate abstract class Money with abstract methods timesとエラーを出してくれます。

辞書型

本では、辞書型のキーとして、独自クラス(通貨変換の、変換前通貨と変換後通貨の文字列を管理)を用います。
しかし、単なる値だけのクラスなので、Pythonでは、タプルをキーとして使用したほうが簡単です。
そのように実装を変更しています。

"""銀行"""
from .money import Money
from .exchanger import CurrencyExchanger
from .expression import Expression

class Bank(CurrencyExchanger):
    """銀行を表します。"""
    def __init__(self):
        self._rates = dict()

    def reduce(self, source: Expression, currency: str) -> Money:
        """式を単純な形に変形する"""
        return source.reduce(self, currency)

    def add_rate(self, fromcurr: str, tocurr: str, rate: int) -> None:
        """貨幣レートの変換登録"""
        self._rates[(fromcurr, tocurr)] = rate

    def rate(self, fromcurr: str, tocurr: str) -> int:
        """変換率を取得"""
        if fromcurr == tocurr:
            return 1
        return self._rates.get((fromcurr, tocurr))

_ratesは、キーが変換前変換後通貨、値が変換率です。

辞書は外部公開しないので、使い方はJavaのクラスと全く変わりません。

ここまでの感想

PythonJavaの違いを実感できて、とても勉強になります。
Pythonunittestは、JUnitを参考にしただけあって、使い勝手もほとんど一緒です。
また、Javaの書き方はPythonでも大抵できます。Javaみたいに、しっかり型を扱おうとすると、引っかかる部分はありますが、上で書いたように様々な手段で実装できます。*5

Pythonを学びたい、と考えている方への一助になればと思います。

あと、12章で一気にやり方が変わり、14章から難易度が上がります。

今後

このまま、第Ⅱ部についても書くつもりでした。
しかし…第二部は本でもPythonを使っています。

さらに問題なのは、本ではxUnitを作るとのことで、Pythonでテストフレームワークを作ろうとします。
もうあるんですよね。使ってきたやつが。

今後どうするかは検討です。
いっそ、unittesttestsuite等を紹介するのもよいのでは、と思っています。

ではまた。

*1:本は、第Ⅰ部はJavaで書かれている

*2:個人的には、継承元に継承先のファクトリメソッドを持つのは問題があるように思える

*3:言語仕様として存在していない

*4:Pythonは多重継承をサポートするため、ABCMetaを使っているクラスをいくつでも継承できる

*5:ソースから型指定を消せば、必要のない苦労。ただ、型指定しても使えるところを示したかった