「Python でビンゴゲーム の確率シミュレーションしてみた」を修正してみた

元ネタ: [Python でビンゴゲーム の確率シミュレーションしてみた]
(http://qiita.com/elzup/items/d532ffa1d326fbf75d01)

読んでておもしろかったから Python のお作法的なところをみながらちょっと修正してみた。

## メイン処理を作る

```py3
if __name__ == '__main__':
    main()
```

*main()* を作るかは好みだけど、スクリプトとして実行したときの処理を分けておくことで、別ターミナルなどでインタラクティブシェルを起動しておいて、モジュールとして import してデバッグしたりするのに便利です。

例えば、*generate_card()* の実装を変えてみて

```diff
 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](http://click.pocoo.org/) や [clip](https://github.com/willyg302/clip.py) のような cli ツールを最近見かけた。でも、用途や必要性があんまりよく分かってない。

```py3
    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 モジュールの使い方も定番ですね。

```py3
logger = getLogger(__name__)
_handler = StreamHandler()
_handler.setLevel(DEBUG)
logger.addHandler(_handler)
...
logger.setLevel(DEBUG)
```

## doctest を書く

本当は元記事の↓に反応してこれが言いたかっただけ。

> docブロックの書き方がいまいちまだわからないので勉強していこうと思う

Python の特徴の1つとして [doctest](http://docs.python.jp/3.4/library/doctest.html) がある。その関数の使い方のドキュメントと簡単なテストを兼ねると一石二鳥だったりする。

元のコードのロジックをいじらないように手を加えてたら、修正したコードの doctest も良いものじゃないのだけど、doctest 書きながら開発しようとすると、使う立場への視野も広がって自然と使いやすい関数やモジュールを実装できたりする。

追記:
うまくいなかったからリベンジした => [「Python でビンゴゲーム の確率シミュレーションしてみた」で doctest を書いてみた](http://qiita.com/t2y/items/d2ded1ddd3d7d234422e)

```py3
    """
    >>> 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* を使ってるからモックがないとテスト書くの難しいかなぁ ... :sweat: 

## 修正したコード

いざやってみたら言いたいことにあうようにあんまりうまく修正できなかった ... :disappointed_relieved:

このコードは Python3 で動く。

```py3:Bingo_kai.py
# -*- 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 %)
```