前回記事の続きです。
『テスト駆動開発』の第Ⅰ章を、Pythonで一通り流したので、感想とか書いてみます。
GitHubのページも貼っておきます。
GitHubのほうは、各章ごとにフォルダを分けており、それぞれREADMEで説明とか感想とか入れています。
ここでは、JavaとPythonの違い*1について述べてみます。
言いたいこと
下は、説明がすごく長いので、先にもってきています。
言いたいことは一つ。
Pythonでも、簡単にテスト駆動ができる
ということです。
組んでいるとき、あまり迷わなかったです。コーディングに使っているVSCodeの優秀さもありますが。
型指定
Pythonは、メソッド引数や変数に、構文上型指定が不要です。
しかし、Python3.5から型アノテーションが導入され、型指定は可能になりました。
今回は、型アノテーションを使ってみる目的で、途中から型指定はすべて入れるようにしています。
- メソッド引数
def times(self, multiplier: int) -> Money: """通貨変換""" return Money(self._amount * multiplier, self.currency)
上のように書いた場合、multiplier: int
の: int
は、構文チェック上無視されます。
同様に、-> Money
も無視されます。
エディタ上は型を反映してくれます。
隠れていますが、上の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'
こういう場合は、
import
をメソッド内で行う- 型アノテーションは文字列で行う
等の対策があります。
継承、インターフェイス
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のクラスと全く変わりません。
ここまでの感想
PythonとJavaの違いを実感できて、とても勉強になります。
Pythonのunittest
は、JUnitを参考にしただけあって、使い勝手もほとんど一緒です。
また、Javaの書き方はPythonでも大抵できます。Javaみたいに、しっかり型を扱おうとすると、引っかかる部分はありますが、上で書いたように様々な手段で実装できます。*5
Pythonを学びたい、と考えている方への一助になればと思います。
あと、12章で一気にやり方が変わり、14章から難易度が上がります。
今後
このまま、第Ⅱ部についても書くつもりでした。
しかし…第二部は本でもPythonを使っています。
さらに問題なのは、本ではxUnitを作るとのことで、Pythonでテストフレームワークを作ろうとします。
…もうあるんですよね。使ってきたやつが。
今後どうするかは検討です。
いっそ、unittest
のtestsuite
等を紹介するのもよいのでは、と思っています。
ではまた。