元ネタ: [Python でビンゴゲーム の確率シミュレーションしてみた]
(http://qiita.com/elzup/items/d532ffa1d326fbf75d01)
読んでておもしろかったから Python のお作法的なところをみながらちょっと修正してみた。
メイン処理を作る
if __name__ == '__main__':
main()
main() を作るかは好みだけど、スクリプトとして実行したときの処理を分けておくことで、別ターミナルなどでインタラクティブシェルを起動しておいて、モジュールとして import してデバッグしたりするのに便利です。
例えば、generate_card() の実装を変えてみて
def generate_card():
@@ -25,12 +33,22 @@
- cards = []
- for i in range(5):
- cards.extend(sample(range(15 * i + 1, 15 * (i + 1)), 5))
- cards[12] = HIT
- return cards
+ def sampling(k=5):
+ for i in range(k):
+ yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)
+
+ from itertools import chain
+ card = list(chain.from_iterable(sampling()))
+ card[12] = HIT
+ return card
ipython でパフォーマンスを計測したりする。
$ ipython3-3.4
In [1]: import orig_Bingo
In [2]: timeit -n 100 orig_Bingo.generate_card()
100 loops, best of 3: 86.4 us per loop
In [3]: import Bingo_kai
In [4]: timeit -n 100 Bingo_kai.generate_card()
100 loops, best of 3: 95.3 us per loop
argparse を使って引数処理する
try ... except でごにょごにょするぐらいなら argparse 使って引数処理してしまおう。argparse の使い方も定型的なものなので慣れてしまえば簡単です。余談だけど、click や clip のような cli ツールを最近見かけた。でも、用途や必要性があんまりよく分かってない。
parser = argparse.ArgumentParser()
parser.set_defaults(num=10000, times=5, verbose=False)
parser.add_argument('-n', '--num', type=int, help='set card number')
parser.add_argument('-t', '--times', type=int, help='set times')
parser.add_argument(
'-v', '--verbose', action='store_true',
help='set verbose mode (loglevel=DEBUG)')
せっかく引数処理するようにしたから logging 使ってデバッグ出力を切り替えるようにしてみる。logging モジュールの使い方も定番ですね。
logger = getLogger(__name__)
_handler = StreamHandler()
_handler.setLevel(DEBUG)
logger.addHandler(_handler)
...
logger.setLevel(DEBUG)
doctest を書く
本当は元記事の↓に反応してこれが言いたかっただけ。
docブロックの書き方がいまいちまだわからないので勉強していこうと思う
Python の特徴の1つとして doctest がある。その関数の使い方のドキュメントと簡単なテストを兼ねると一石二鳥だったりする。
元のコードのロジックをいじらないように手を加えてたら、修正したコードの doctest も良いものじゃないのだけど、doctest 書きながら開発しようとすると、使う立場への視野も広がって自然と使いやすい関数やモジュールを実装できたりする。
追記:
うまくいなかったからリベンジした => 「Python でビンゴゲーム の確率シミュレーションしてみた」で doctest を書いてみた
"""
>>> check_bingo([False, True, False])
False
>>> check_bingo([True] * 5)
True
>>> check_bingo([False, False, True, False, False] * 5)
True
>>> check_bingo([
... True, False, False, False, False,
... False, True, False, False, False,
... False, False, True, False, False,
... False, False, False, True, False,
... False, False, False, False, True,
... ])
True
"""
random.sample を使ってるからモックがないとテスト書くの難しいかなぁ ...
修正したコード
いざやってみたら言いたいことにあうようにあんまりうまく修正できなかった ...
このコードは Python3 で動く。
# -*- coding: utf-8 -*-
u"""BINGO の確率をシミュレーションするスクリプト
input:
times = 抽選機を回す回数, card_num = カードの数
outpu:
1本でも揃う確率
"""
import argparse
import sys
from logging import DEBUG, StreamHandler, getLogger
from random import sample
# 穴
HIT = True
NOHIT = False
# ログ出力
logger = getLogger(__name__)
_handler = StreamHandler()
_handler.setLevel(DEBUG)
logger.addHandler(_handler)
def generate_card():
u"""カードを生成
5x5で中心(3行3列)に穴が空いたカード
B(1列目) 1 - 15
I(2列目) 16 - 30
N(3列目) 31 - 45
G(4列目) 46 - 60
O(5列目) 61 - 75
:returns: array card, length=25
>>> from math import floor
>>> card = generate_card()
>>> card_len = len(card)
>>> card_len == 25
True
>>> card[floor(card_len / 2)]
True
"""
def sampling(k=5):
for i in range(k):
yield sample(range(15 * i + 1, 15 * (i + 1) + 1), k)
from itertools import chain
card = list(chain.from_iterable(sampling()))
card[12] = HIT
return card
def check_bingo(card):
u"""BINGOしているかどうか
判定のみかえす
param: array
:returns: boolean
>>> check_bingo([False, True, False])
False
>>> check_bingo([True] * 5)
True
>>> check_bingo([False, False, True, False, False] * 5)
True
>>> check_bingo([
... True, False, False, False, False,
... False, True, False, False, False,
... False, False, True, False, False,
... False, False, False, True, False,
... False, False, False, False, True,
... ])
True
>>> check_bingo([
... False, False, False, False, True,
... False, False, False, True, False,
... False, False, True, False, False,
... False, True, False, False, False,
... True, False, False, False, False,
... ])
True
"""
if card.count(HIT) < 5:
return False
for i in range(5):
if all(card[i * 5:(i + 1) * 5]): # horizontal
return True
for i in range(5):
if all(card[i:i + 21:5]): # vertical
return True
if all(card[0:25:6]) or all(card[4:21:4]): # skew
return True
return False
def print_card(card):
msg = ''
for i, v in enumerate(card):
if v == HIT:
v = 'o'
elif v == NOHIT:
v = 'x'
msg += '%3s' % v
if i % 5 == 4:
msg += '\n'
logger.debug(msg)
def parse_argument(argv=None):
"""
>>> parse_argument([])
Namespace(num=10000, times=5, verbose=False)
>>> parse_argument(['-n', '3', '-t', '30', '-v'])
Namespace(num=3, times=30, verbose=True)
"""
parser = argparse.ArgumentParser()
parser.set_defaults(num=10000, times=5, verbose=False)
parser.add_argument('-n', '--num', type=int, help='set card number')
parser.add_argument('-t', '--times', type=int, help='set times')
parser.add_argument(
'-v', '--verbose', action='store_true',
help='set verbose mode (loglevel=DEBUG)')
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
if args.verbose:
logger.setLevel(DEBUG)
return args
def do_bingo(args):
card = generate_card()
lots = dict(sample([(i, HIT) for i in range(1, 76)], args.times))
card_hole = [lots.get(i, NOHIT) for i in card]
logger.debug('lots: {}\n'.format(sorted(lots.keys())))
print_card(card)
print_card(card_hole)
return card_hole
def main():
args = parse_argument()
hit_count = 0
for i in range(args.num):
card_hole = do_bingo(args)
if check_bingo(card_hole):
hit_count += 1
print(str(args.times) + u"回目で1本でもBINGOしてる確率:")
p = hit_count / args.num
print("%s (%.1f %%)" % (str(p), p * 100))
if __name__ == '__main__':
main()
こんな感じ。
$ python Bingo_kai.py -n 3 -t 45 -v
...
lots: [1, 3, 8, 9, 10, 11, 12, 14, 16, 17, 19, 21, 22, 24, 26, 28, 29, 30,
31, 32, 33, 35, 36, 37, 39, 40, 41, 45, 46, 47, 49, 52, 55, 57, 60,
61, 63, 64, 67, 68, 69, 71, 72, 73, 75]
3 11 5 o 12
20 18 16 26 17
37 42 o 32 40
50 47 48 51 49
67 66 70 74 64
o o x o o
x x o o o
o x o o o
x o x x o
o x x x o
45回目で1本でもBINGOしてる確率:
0.6666666666666666 (66.7 %)