マルチスレッドはてなキーワードしりとりを改造・その3

先日のコードに頂いたコメントを参考に一部を書き直しました。

今回最大の変更点は入力を受け付けるmulti_input_reader関数をジェネレータにし、従来main関数に書いた"while True"で行なっていた全体のループをこのジェネレータで管理するようにしました。
そしてmulti_input_reader関数をmain関数の引数として受け取るようにしました。ただmulti_input_reader関数がmain関数のローカル変数である"usedwords"を引数としている都合で、どうにもmainとmulti_input_readerの分離が悪く綺麗にかけていません(^_^;)
usedwordsをグローバル変数にすれば見かけは綺麗になるのですが、むやみにグローバル変数を増やすのもすっきりしません。
今回テストを書いてみようとしたのですが、上記の問題がテストを複雑にするのでここを直してからにします。

入力を受け付けた際にその入力内で単語が重複していればRepeatedWordsErrorという例外を示す処理についてです。
furigana.examというしりとりのルールに関するチェックを行うメソッドで「同じ単語が二度使われないか」を調べているため、この入力時のチェックは処理上必須ではありません。
ただこの処理を省くと例えば「アクア、アクア、アクア、アクア」と入力した場合4つのスレッドが「アクア」という単語を並行でAPIに問い合わせることになってしまうので一応残しました。子スレッドの作成時点でParallelAsk._furiganadictにすでに登録がある単語はAPIへの問い合わせを行わないのですが、2つ目の子スレッドが作成される時点ではまだ1つ目の子スレッドに対してAPIからの返答が完了していない可能性が高く、辞書への登録もまだのためAPIに同じ単語を問い合わせてしまいます。このことは当初意図した動作である「同じ単語を複数回APIに問い合わせない」に反してしまうのでRepeatedWordsError削除するならばこの問題を解決する処理を追加してからにしたいと思います。

# coding: utf-8

import shiritori
import threading

WORDS_MAX = 10  # 一度に入力を受け付ける最大単語数


class PrlSupportHFD(shiritori.HatenaFuriganaDict):
    """親クラスの辞書登録処理に排他制御を加える"""
    def __init__(self):
        self.lock = threading.Lock()
        shiritori.HatenaFuriganaDict.__init__(self)

    def __setitem__(self, key, value):
        self.lock.acquire()
        shiritori.HatenaFuriganaDict.__setitem__(self, key, value)
        self.lock.release()


class ParallelAsk(threading.Thread):
    """マルチスレッド関連処理"""
    _furiganadict = PrlSupportHFD()

    def __init__(self, word):
        self.word = word
        threading.Thread.__init__(self)

    def run(self):
        self.result = ParallelAsk._furiganadict[self.word]

    @classmethod
    def get_result(cls, words):
        """問い合わせ結果をwordsの順に
           (ふりがな, 元の単語)のタプルで
           返すジェネレータ
        """
        if len(words) > WORDS_MAX:
            raise WordsInputError(value=WORDS_MAX)

        if len(words) != len(set(words)):
            raise RepeatedWordsError()

        # 子スレッドをセット
        askers = []
        for word in words:
            asker = cls(word)
            askers.append(asker)
            asker.start()

        #各スレッドの結果をFuriganaインスタンス、元単語のタプルで返す
        for (asker, word) in zip(askers, words):
            asker.join()
            furigana = shiritori.Furigana(asker.result, word)

            yield (furigana, word)


def multi_input_reader(usedwords):
    """入力を受けとり単語のリストを作成する
       ジェネレータ
    """
    while True:
        wordsstr = raw_input(u"次の単語を入力してください。次は"
                             + str(len(usedwords) + 1)
                             + u"語目です。")

        wordsstrU = unicode(wordsstr).replace(u"、", u",")

        wordsU = wordsstrU.split(u",")

        yield wordsU


class WordsInputError(shiritori.ShiritoriRuleError):
    def __str__(self):
        return (u"入力した単語が多すぎます。一度に入力できるのは"
               + str(self.value)
               + u"語までです。")


class RepeatedWordsError(shiritori.ShiritoriRuleError):
    def __str__(self):
        return u"入力した単語に重複があります。"


def main(initial=u"あ", inputsource=multi_input_reader):
    usedwords = {}

    print (u"最初のもじは「" + initial + u"」です")

    for wordsU in inputsource(usedwords):
        try:
            for (furigana, word) in ParallelAsk.get_result(wordsU):
                if word in usedwords:  # 使用済みでないか確認
                    raise shiritori.UniquenessError(word, usedwords[word])

                furigana.exam(initial, word)  # 冒頭、末尾文字確認
                initial = furigana.creatIL()
                print word
                usedwords[word] = len(usedwords) + 1

        except shiritori.CrtShiritoriError, e:
            print e
            break
        except shiritori.ShiritoriRuleError, e:
            print e
            continue

    print u"終了です"

if __name__ == "__main__":
    main()