概要
前回 宣言した通り読んだので振り返ってみようと思います。
見せつけるようなものではないですがコードはこちら↓
以下、 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 には抽象基底クラスがあります。
ドキュメントを読んでも一ミリもピンと来ないので、実際に遊んでみて挙動を確かめてみます。
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つ失敗することを確認する。
- 小さい変更を行う。
- 再びテストを実行し、すべて成功することを確認する。
- リファクタリングを行い、重複を除去する。
のサイクルをひたすら繰り返す、ということで、この流れの中で個別のクラスの挙動(Java なので本書ではオブジェクト指向が根底にあります)が共通化され、上位のクラスを継承するという形で重複が排除されていきます。
なので、進めていくと型の指定をちょこちょこ書き直したりする箇所が出てくるのですが、Python は静的片付け言語ではないのでここら辺の手続きをちょいちょいすっ飛ばすことになります。 また、Python には変数の宣言はなく( global とか nonlocal とかそこらへんの話はのぞいて)いきなり代入文がくるので、ここら辺の話も飛ばすことになります。
Java よりももう少し気楽に進められるような気がしました。 もちろん Java の方がガチガチに進めていってる感があるのでよりミスは起こりにくそうな雰囲気はありました。
だんだん低次元な読書感想文になってきましたね。
プライベート変数
あと Java と Python の違いでよくあるのが Python にはプライベート変数がない、という話ですね。
こちらのドキュメントを読んでいただければわかるように実質的にプライベート変数を定義するマングリング(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 部は Python で Junit のようなものを作ろうというお話なのでそれも読んだら適当にまとめと感想を書きます