魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

テスト駆動開発の第 I 部 多国通貨 読んだ

概要

前回 宣言した通り読んだので振り返ってみようと思います。

見せつけるようなものではないですがコードはこちら↓

github.com

以下、 Java で書かれたコードを Python に翻訳する上で考えたこと、学んだことやらを書いていきます。

テストコード

Python の標準ライブラリでテストと言えば unittest しか考えていなかったのですが、もっとシンプルにいくのであれば assert 文を使うのもアリだったかなと思いました。 といっても出力内容がリッチではないので、多分テストがコケたときの原因分析が面倒になる可能性はあります。

assert 文の文法は次のような感じです。

assert 条件文, メッセージ

このメッセージは条件文が False のときに出力されるものです。

条件文が True のときは何も出力されません。

>>> assert 1 == 1, '一致しません'
>>>

条件文が False になると AssertionError のメッセージとして出力されます。

>>> assert 1 == 2, '一致しません'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: 一致しません

unittest を使うともっと JUnit っぽく書けるのでわかりやすいです。

class TestMoney(unittest.TestCase):
    def test_multiplication(self):
        five = Money.dollar(5)
        self.assertEqual(Money.dollar(10), five.times(2))
        self.assertEqual(Money.dollar(15), five.times(3))

成功したときはこんな出力です。(テストメソッドが全部で12本あるときです)

❯ python -m unittest tests/test_money.py
............
----------------------------------------------------------------------
Ran 12 tests in 0.000s

OK

適当に書き換えて失敗させるとこんな出力です。 わかりやすいですね。

❯ python -m unittest tests/test_money.py
....F.......
======================================================================
FAIL: test_multiplication (tests.test_money.TestMoney)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/hoge-user/tdd_python/tests/test_money.py", line 13, in test_multiplication
    self.assertEqual(Money.dollar(15), five.times(2))
AssertionError: <src.money.Money object at 0x110627df0> != <src.money.Money object at 0x110627ca0>

----------------------------------------------------------------------
Ran 12 tests in 0.001s

FAILED (failures=1)

....F........ が成功したテスト、 F が失敗したテストを表します。 テストコードやテスト対象のコード内で予期せぬ(つまりキャッチしていない)例外が発生した場合は単純にエラーなので E が出力されます。

ここで少し引っかかってしまいそうなのは、先ほどの出力内容をみて ..F. となっていた場合上から3つ目のテストが失敗しているとは 限らない ということですね。

実行される順序は保証されません。 なので、失敗しているメッセージはちゃんとトレースバックを読まないとわからないということ、そしてもっと大事なのが各テストを独立させることが重要です。

新人の頃は上から順に実行したらちょうどゴミが残らず綺麗になるわい、と思ってオブジェクトを生成して何かしら処理をして最後にそれを消したり、みたいな処理を書いて見事に失敗する、ということがありました。

みなさんも気を付けましょう。

コードの変更でテストが失敗するのは望ましいことですがテストの追加によって既存のテストがコケる、というのはアレですからね。

インターフェイス

例えば次のようなコードが掲載されていますが、

public Money reduce(Bank bank, String to) {
    int rate = bank.rate(currency, to);
    return new Money(amount / rate, to);
}

これを Python で書いた場合は次のようになるかと思います。

def reduce(self, bank, to):
        rate = bank.rate(self._currency, to)
        return Money(self._amount / rate, to)

簡単ですね。

基本的にはこういう書き換えはあまり深く考えずできてしまいますが、一つ簡単にはいかないものがあります。 インターフェイスです。

そもそも Java におけるインターフェイスとはなんだったかというのを考えます。 インターフェイスは平たく言うとクラスが持つべき変数(メンバ)、メソッドを定義しておくものです。 このインターフェイスですが、調べた感じ Python にはありません。 なので抽象クラスで代用する方法がいろんなところで解説されていました。 抽象クラスって抽象メソッドという中身のないメソッドだけを宣言して、それを継承したクラスは必ずその中身を定義しないと使えない、つまり「これらのメソッドをちゃんと定義してください」というのを規定するものです。

あれ?インターフェイスと抽象クラスの違いって何でしたっけ?

なんか調べると Java7 と Java8 でここの違いがよりわかりにくくなったみたいです。 もう多重継承できるか否かぐらいの違いしかないっぽいです。しらんけど。

多重継承というのは複数のクラスから継承するということで、 Java ではできないことになっています。 で、Javaインターフェイスでは継承( Extend )ではなく実装( Implement )と用語が違いますが、複数のインターフェイスを実装できます。

なので、抽象クラスとインターフェイスで実質的に同じことができるようになった現状では、インターフェイスを使えば実質的に多重継承ができるという訳ですね。 実際にコード書いて確かめようという気が起きないのでこの程度のフワッとした話で一旦片付けようと思います。 この記事もインターフェイスと抽象クラスの違いを理解しようとして嫌になって二週間以上放置しているので……

Python にはインターフェイスはありませんが、そもそも多重継承が可能なので、もうインターフェイスを使わなくても Java と同じことができるから、抽象クラスで全部やっちまおうぜという解釈なのだと思うことにします。

抽象クラスと抽象メソッド

Python には抽象基底クラスがあります。

docs.python.org

ドキュメントを読んでも一ミリもピンと来ないので、実際に遊んでみて挙動を確かめてみます。

class Animal:
    def name(self):
        print('just animal')

    def fuga(self):
        return self.name()


class Dog(Animal):
    def name(self):
        print('dog')

オブジェクト指向のサンプルではお馴染みの、動物クラスを継承させた犬クラスを作るやつです。 と言っても細部を作るのがクソ面倒だったので見ればわかるように中身のメソッドは適当です。

こいつに対して、まず Animal クラスの fuga メソッドを直接呼び出すとどうなるかというと

>>> animal = Animal()
>>> animal.fuga()
just animal

もちろん fuga メソッドは name メソッドを返すので、 Animal クラスの name メソッドの通り文字列 'just animal' を標準出力に出すゴミみたいな挙動が確認できます。

>>> dog = Dog()
>>> dog.fuga()
dog

で、これを継承した Dog クラスの name メソッドは 'dog' という文字列を表示するこれまたカスみたいな挙動が確認できます。

ここの挙動って VSCode のリファレンス元をリンクで飛べる機能を使うと継承する元のメソッドの方に飛んでしまうので、継承した先の実際の挙動が確認できないんですよね。なので最初結構混乱してしまいました。

これを、 Animal クラスを抽象クラスにするとどうなるかというと

from abc import ABCMeta, abstractmethod


class Animal(metaclass=ABCMeta):
    @abstractmethod
    def name(self):
        pass

    @abstractmethod
    def fuga(self):
        pass


class Dog(Animal):
    def name(self):
        print('dog')

    def fuga(self):
        return self.name()

こうなります。抽象基底クラスを継承することで抽象クラスとしての性質を付与できるという寸法です。

fuga メソッドの挙動の定義が Dog クラスに押し付けられてますね。実行するとこうなります。

>>> dog = Dog()
>>> dog.fuga()
dog

抽象クラスはインスタンスを生成できないので、これでエラーが出ます。

>>> animal = Animal()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Animal with abstract methods fuga, name

抽象メソッドがどれか教えてくれるのはありがたいですね。 まぁ一旦は抽象クラスについての掘り下げはこんなもんでいいしょう(適当)。 もっとオブジェクト指向大好き人間になってから追求したいと思います。

進め方の違い

テスト駆動開発の進め方として本書の中で繰り返し述べられているのが、

  1. 小さいテストを1つ書く。
  2. すべてのテストを実行し、1つ失敗することを確認する。
  3. 小さい変更を行う。
  4. 再びテストを実行し、すべて成功することを確認する。
  5. リファクタリングを行い、重複を除去する。

のサイクルをひたすら繰り返す、ということで、この流れの中で個別のクラスの挙動(Java なので本書ではオブジェクト指向が根底にあります)が共通化され、上位のクラスを継承するという形で重複が排除されていきます。

なので、進めていくと型の指定をちょこちょこ書き直したりする箇所が出てくるのですが、Python は静的片付け言語ではないのでここら辺の手続きをちょいちょいすっ飛ばすことになります。 また、Python には変数の宣言はなく( global とか nonlocal とかそこらへんの話はのぞいて)いきなり代入文がくるので、ここら辺の話も飛ばすことになります。

Java よりももう少し気楽に進められるような気がしました。 もちろん Java の方がガチガチに進めていってる感があるのでよりミスは起こりにくそうな雰囲気はありました。

だんだん低次元な読書感想文になってきましたね。

プライベート変数

あと JavaPython の違いでよくあるのが Python にはプライベート変数がない、という話ですね。

docs.python.org

こちらのドキュメントを読んでいただければわかるように実質的にプライベート変数を定義するマングリング(Mangling)というテクニックがあるのでいい感じに使えば良いと思います。

Hoge クラスに __hoge という変数を定義すると内部的に _Hoge__hoge という名前に置換されます。 これは Hoge クラス内では __hoge という名前で問題なく呼び出せるのですが、 こいつを Fuga クラスで継承したとき、 同じようにアクセスしようとすると _Fuga__hoge なんて変数はないよ、と怒られて実質的にプライベート変数としての機能が果たせるよ、という感じです。

標準ライブラリでここらへんの技法が駆使されまくっているので、知らなかったころは読んでて頭おかしなるわいと思ってました。

Java だとアクセス修飾子に public、protected、private とありますが、protected はどうやって Python で再現するんですかね?

継承したクラスからであればアクセスできる変数ということですが、これは変数名の頭にアンダースコアを一つつけた _hoge を慣習的に使うらしいです。

あくまで慣習なので普通にアクセスできちゃいますけどね。

class Animal:
    _age = 100  # protected のつもり
    __height = 1000  # private のつもり
    def name(self):
        print('just animal')

    def fuga(self):
        return self.name()


class Alien:
    def get_age():
        print(Animal._age)  # アクセスできる

    def get_height1():
        print(Animal.__height)  # アクセスできない

    def get_height2():
        print(Animal._Animal__height)  # アクセスできる

動物クラスを継承していなエイリアンクラスを定義してみました。 その上で _age__height という変数にアクセスできるか試してみます。

>>> alien = Alien
>>> alien.get_age()
100

protected だったら継承していないのでアクセスできないはずですが、この記法はあくまで慣習的なものなので Animal.__height で普通にできます。

>>> alien.get_height1()
Traceback (most recent call last):
  File "extend_sample.py", line 36, in <module>
    alien.get_height1()
  File "extend_sample.py", line 21, in get_height1
    print(Animal.__height)
AttributeError: type object 'Animal' has no attribute '_Alien__height'

一方、 __height についてはマングリングによって名前が __height から _Alien__height に置換されるので、そんなものは定義されていないと怒られていますね。

>>> alien.get_height2()
1000

でも名前が書き換わっているだけなのでこんな感じに Animal._Animal__height という風にしてやれば呼び出せます。

まとめ

記事にまとまりがなさすぎる。 包括的にオブジェクト指向について語るかテスト駆動について語るかどちらかにすればよかった感がありますね。 これ以上寝かしておくのは辛いのでもうこれで公開します。

第 II 部は PythonJunit のようなものを作ろうというお話なのでそれも読んだら適当にまとめと感想を書きます