概要および趣旨
概要
前回実装したDiceクラス↓には大きな欠陥があるらしいよ(記事コメント参照)。
Javaに慣れているとインスタンス変数にしか見えないものが、Pythonの世界ではクラス変数の扱いになるんだって。怖い怖い。
どうまずいの?
不具合を放っておくと、別インスタンスを生成するたびにシード値が勝手に(いや実装通りなんだけど)書き変わり、複数のDiceインスタンスで同じシード値を共有してしまう。
「呼び出し側がシード値Aのつもりで使ってたダイスが実はシード値Bで動いていた」
ってのは確かにまずい。
「プログラムは思った通りに動かない。書いた通りに動く」っていう古の賢人の言葉を思い出したよ。
今回やること
というわけで、前回の実装のダメな点を追加テストで明らかにしたいんだけど、その前段階としてテストコードをDiceクラスのモジュールから分離しようと思う。
なので、今回やることは以下の3つ。
- テストコードを別モジュールに分離
- 別モジュールに新たなテストコードを追加
- 追加したテストコードを動かして不具合を明らかにする
ボリューム満点だよ。
既存テストコードの分離
前回作った「Dice.py」と同じ階層にモジュール「DiceTest.py」を新規作成。
IDEを使ってるなら、ソースフォルダを「src」と「test」に分けてDice.pyをsrc配下、DiceTest.pyをtest配下に置くと見通しが良くなると思う。
DiceTest.pyが作成出来たら、前回記事の
if __name__ == "__main__":
以下の記述をカット&ペースト。
そのままだとビルドエラー(この用語が適切かわからんけど)が発生するので、冒頭に以下のインポート宣言を追加。
from Dice import Dice
import time
結果、以下のコードになっているはず。
'''
Created on 2022/10/09
@author: 芸夢 作郎
'''
import time
from Dice import Dice
if __name__ == "__main__":
# シード値に350を指定する
diceExplicit1 = Dice(350)
print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit1.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp1.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicit2 = Dice(350)
print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit2.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp2.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicitTheOtherSide = Dice(350)
print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
for i in range(0, 100, 1):
# 2000面ダイスを100回転がす
print(diceExplicitTheOtherSide.roll(2000))
print()
念のため実行すると、昨日と同様の結果になる。
本体もテストコードもロジックには手を入れていないから当たり前。
昨日は実行結果を貼るのを端折ったけど、今日は貼っておくよ。
後々の比較で必要になるし。
テスト1実行開始
① シード値指定1回目/シード値: [350]
912
372
822
185
412
867
687
321
668
364
(中略)
② シード値にタイムスタンプを使用1回目/シード値: [1665323375739]
496
524
799
570
591
758
754
912
90
807
(中略)
③ シード値指定2回目/シード値: [350]
912
372
822
185
412
867
687
321
668
364
(中略)
④ シード値にタイムスタンプを使用2回目/シード値: [1665323379759]
14
908
172
942
431
861
449
142
29
379
(中略)
⑤ シード値指定3回目(面数を変更)/シード値: [350]
1823
744
1644
369
823
1733
1374
642
1335
727
(以下略)
新たなテストコードを追加
複数のダイスを同時に転がして出目を横並びで確認するテストケースを追加するよ。
期待値は「シード値と出目の最大値が同じであれば、必ず先日と同様の結果が返ってくること(=各ダイスのシード値が相互に干渉していないこと)」。
確認するためのコードは以下の通り。
# シード値に350を指定する
dice350A = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
time.sleep(2)
# シード値に350を指定する
dice350B = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
time.sleep(2)
# シード値に700を指定する
dice700 = Dice(700)
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print('ロール{}回目'.format(i + 1))
print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
print()
print()
何も考えずにDiceTest.pyの末尾に追記してもいいんだけど、確認観点が違うので関数化して別管理したいよね。
というわけで、2つのテストコードを関数化したDiceTest.pyは以下の通り。
'''
Created on 2022/10/09
@author: 芸夢 作郎
'''
import time
from Dice import Dice
def test1():
print('テスト1実行開始')
# シード値に350を指定する
diceExplicit1 = Dice(350)
print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit1.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp1.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicit2 = Dice(350)
print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit2.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp2.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicitTheOtherSide = Dice(350)
print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
for i in range(0, 100, 1):
# 2000面ダイスを100回転がす
print(diceExplicitTheOtherSide.roll(2000))
print()
return
def test2():
print('テスト2実行開始')
# シード値に350を指定する
dice350A = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
time.sleep(2)
# シード値に350を指定する
dice350B = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
time.sleep(2)
# シード値に700を指定する
dice700 = Dice(700)
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print('ロール{}回目'.format(i + 1))
print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
print()
print()
return
if __name__ == "__main__":
test1()
test2()
テスト
確認したいのはテスト2の実行結果。
コンソールログを確認すると、結果は以下の通り。
テスト2実行開始
ロール1回目
シード値350のダイスロール1/シード値: [350] 出目: [171]
シード値350のダイスロール2/シード値: [350] 出目: [925]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [197]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [459]
シード値700のダイスロー2/シード値: [700] 出目: [717]
ロール2回目
シード値350のダイスロール1/シード値: [350] 出目: [221]
シード値350のダイスロール2/シード値: [350] 出目: [855]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [544]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [630]
シード値700のダイスロー2/シード値: [700] 出目: [811]
ロール3回目
シード値350のダイスロール1/シード値: [350] 出目: [332]
シード値350のダイスロール2/シード値: [350] 出目: [871]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [6]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [622]
シード値700のダイスロー2/シード値: [700] 出目: [862]
(中略)
ロール18回目
シード値350のダイスロール1/シード値: [350] 出目: [504]
シード値350のダイスロール2/シード値: [350] 出目: [501]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [168]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [159]
シード値700のダイスロー2/シード値: [700] 出目: [605]
ロール19回目
シード値350のダイスロール1/シード値: [350] 出目: [625]
シード値350のダイスロール2/シード値: [350] 出目: [666]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [407]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [187]
シード値700のダイスロー2/シード値: [700] 出目: [567]
ロール20回目
シード値350のダイスロール1/シード値: [350] 出目: [154]
シード値350のダイスロール2/シード値: [350] 出目: [428]
シード値にタイムスタンプを使用したダイスロール1/シード値: [1665318474352] 出目: [988]
シード値にタイムスタンプを使用したダイスロール2/シード値: [1665318478380] 出目: [444]
シード値700のダイスロー2/シード値: [700] 出目: [845]
(以下略)
あれあれあれ、テスト1の実行結果と全然違う。
この乱数列は一体何だろう。
前稿の指摘通り、Diceクラス内のクラス変数が悪さをしているのかな。
というわけで、以下のコードをtest1に追加して実行してみる。
time.sleep(2)
# シード値に700を指定する
diceSeedVal700 = Dice(700)
print('⑥ シード値700/シード値: [{}]'.format(diceSeedVal700.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceSeedVal700.roll(1000))
print()
結果は以下の通り。
⑥ シード値700/シード値: [700]
171
925
197
459
717
221
855
544
630
811
332
871
6
622
862
(中略)
504
501
168
159
605
625
666
407
187
567
154
428
988
444
845
較べれば判るとおり、テスト2のロール1回目~20回目の出目の並びと、テスト1(追加)の出目の並びが完全に一致している。
シード値の異なる複数のダイスを並列で転がしていたつもりが、シード値700(最後に指定したシード値)のダイス1個(見た目複数に見える)を一生懸命転がしていたことになる。
確かにこれはおかしい。
おわりに
というわけで、次稿でこの不具合を改修するよ。
今回コードの改修が多かったので、各モジュールの最終形を共有するよ。
'''
Created on 2022/10/08
@author: 芸夢 作郎
'''
import random
import time
class Dice():
# TODO インスタンス変数を使用するよう改修
seedValue = None
def __init__(self, aSeedValue = None):
if (aSeedValue == None):
self.seedValue = int(time.time() * 1000)
else:
self.seedValue = aSeedValue
random.seed(self.seedValue)
return
# TODO 最大面数(aMaxSidesNum)に整数値以外が指定された場合の対処法も考慮
def roll(self, aMaxSidesNum):
ret = random.randint(1, aMaxSidesNum)
return ret
'''
Created on 2022/10/09
@author: 芸夢 作郎
'''
import time
from Dice import Dice
def test1():
print('テスト1実行開始')
# シード値に350を指定する
diceExplicit1 = Dice(350)
print('① シード値指定1回目/シード値: [{}]'.format(diceExplicit1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit1.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
print('② シード値にタイムスタンプを使用1回目/シード値: [{}]'.format(diceTimestamp1.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp1.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicit2 = Dice(350)
print('③ シード値指定2回目/シード値: [{}]'.format(diceExplicit2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceExplicit2.roll(1000))
print()
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
print('④ シード値にタイムスタンプを使用2回目/シード値: [{}]'.format(diceTimestamp2.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceTimestamp2.roll(1000))
print()
time.sleep(2)
# シード値に350を指定する
diceExplicitTheOtherSide = Dice(350)
print('⑤ シード値指定3回目(面数を変更)/シード値: [{}]'.format(diceExplicitTheOtherSide.seedValue))
for i in range(0, 100, 1):
# 2000面ダイスを100回転がす
print(diceExplicitTheOtherSide.roll(2000))
print()
time.sleep(2)
# シード値に700を指定する
diceSeedVal700 = Dice(700)
print('⑥ シード値700/シード値: [{}]'.format(diceSeedVal700.seedValue))
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print(diceSeedVal700.roll(1000))
print()
return
def test2():
print('テスト2実行開始')
# シード値に350を指定する
dice350A = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp1 = Dice()
time.sleep(2)
# シード値に350を指定する
dice350B = Dice(350)
time.sleep(2)
# シード値を指定しない
diceTimestamp2 = Dice()
time.sleep(2)
# シード値に700を指定する
dice700 = Dice(700)
for i in range(0, 100, 1):
# 1000面ダイスを100回転がす
print('ロール{}回目'.format(i + 1))
print('シード値350のダイスロール1/シード値: [{}] 出目: [{}]'.format(dice350A.seedValue, dice350A.roll(1000)))
print('シード値350のダイスロール2/シード値: [{}] 出目: [{}]'.format(dice350B.seedValue, dice350B.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール1/シード値: [{}] 出目: [{}]'.format(diceTimestamp1.seedValue, diceTimestamp1.roll(1000)))
print('シード値にタイムスタンプを使用したダイスロール2/シード値: [{}] 出目: [{}]'.format(diceTimestamp2.seedValue, diceTimestamp2.roll(1000)))
print('シード値700のダイスロー2/シード値: [{}] 出目: [{}]'.format(dice700.seedValue, dice700.roll(1000)))
print()
print()
return
if __name__ == "__main__":
test1()
test2()
ではまた。