はてなキーワードしりとりを改造・その6
先の記事で「例外処理をクラスにする意義がわかった」なんて書いてしまったのですが分かっていませんでした(^_^;)
それどころか大きな勘違いをしており先のコードには致命的な間違いがありました。
またクラス変数についても重大な勘違いをしているところがあったのでこの2つの点についてまず整理したいと思います。
except節について
Pythonのドキュメントに
except 節のクラスは、例外と同じクラスか基底クラスのときに互換 (compatible)となります。
http://www.python.jp/doc/release/tutorial/classes.html#tut-exceptionclasses
とあるようにexcept節は同じクラス以外にそのクラスから派生したクラスも捕捉します。前回のコードでmain関数に
except ShiritoriRuleError as e: print e continue except CrtShiritoriError as e: print e break
と書いていましたがCrtShiritoriErrorはShiritoriRuleErrorの派生クラスなのでどちらの例外も
except ShiritoriRuleError as e:
の部分でキャッチされてしまい、「ん」のつく単語を入力してもしりとりが終了しない不具合となっていました。
上記の例ですとexcept節を逆にし、CrtShiritoriErrorを先に書けば良いようです。
クラス変数について
以前クラスの超基礎という記事を書いた際に
class Test(object): a = 5
と
class Test(object): def __init__(self): self.a = 5
のaというプロパティが全く同じ物であるかのように勘違いしており、そのように書いてしまったのですがこれは全く異なる物でした。
前者ではaはクラス変数であるのに対し、後者ではインスタンス変数です。
class Test(object): a = 5 class Test2(object): def __init__(self): self.a = 5 test = Test() test2 = Test2() #インスタンスからはまずインスタンス変数が参照され #存在しなければクラス変数が参照される print test.a # 5 (クラス変数) print test2.a # 5 (インスタンス変数) #クラス変数なので「クラス名.属性名」で参照可能 print Test.a # 5 #インスタンス変数は「クラス名.属性名」では参照できない print Test2.a # AttributeError
クラス変数はクラス定義の際に一度だけ定義されるのに対し、インスタンス変数はインスタンス生成の都度定義される。そのため内容に引数による変化がないのであればクラス変数にするべき、のようです。
今回の改造点
- except節の勘違いに由来する不具合を修正したのにあわせてこれまでShiritoriRuleErrorでまとめてキャッチし、内部でifを使って分類していた例外をすべて個別の派生クラスに分割しました。
- しりとりの「次の文字」を作成する際に使用する辞書を従来はインスタンス変数だったものをクラス変数に変更し、インスタンスが作られるたびに再定義するムダを省きました。
- 「次の文字」生成はtranslateメソッドを使うようにしました。
# coding: utf-8 import urllib import urllib2 from xml.dom.minidom import parse def input_reader(usedwords): """入力を受け取ってunicodeオブジェクトにして返す""" word = raw_input(u"次の単語を入力してください。次は" + str(len(usedwords) + 1) + u"語目です。") wordU = unicode(word, "utf-8") if wordU in usedwords: raise UniquenessError(usedwords[wordU]) return wordU class HatenaFuriganaDict(dict): """入力された単語とそのふりがなの組み合わせを辞書として管理する はてなキーワードに登録されていなかった単語の値はNone APIから正しいxmlが受信できない単語の値はFalseとする """ @staticmethod def _ask_hatena(word): """__missing__から呼ばれてwordをはてなキーワードAPIに問い合わせる""" #URLエンコード encoded_word = urllib.quote(word.encode("utf-8")) #URLを作成 requestURL = ("http://search.hatena.ne.jp/keyword?word=" + encoded_word + "&mode=rss&ie=utf8&page=1") return urllib2.urlopen(requestURL, None) def __missing__(self, word): """この辞書に登録されていないwordが参照された場合に呼ばれ APIに問い合わせを行い返答する。さらにその結果を辞書に登録する """ try: dom = parse(self._ask_hatena(word)) except: self[word] = False return self[word] items = dom.getElementsByTagName("item") hatenaNS = "http://www.hatena.ne.jp/info/xmlns#" #XMLのNS if items and (items[0].getElementsByTagName("title"))[0].firstChild.data == word: self[word] = (items[0].getElementsByTagNameNS(hatenaNS, "furigana"))[0].firstChild.data else: self[word] = None return self[word] class Furigana(unicode): """しりとりのルールに関連する処理を担う""" # 語尾置換用文字列 rep_letters = {u"ゃ":u"や", u"ゅ":u"ゆ", u"ょ":u"よ", u"を":u"お", u"っ":u"つ" , u"ぁ":u"あ", u"ぃ":u"い", u"ぅ":u"う", u"ぇ":u"え", u"ぉ":u"お"} to_replace = {} # Unicodeコードポイントに置き換え for k, v in rep_letters.iteritems(): to_replace[ord(k)] = v def __init__(self, furigana): if furigana is None: raise DataNotFoundError() elif furigana is False: raise XMLError() else: unicode.__init__(self, furigana) def exam(self, initial): """最初の文字と最後の文字を調べて適格か否か判断""" if not self.startswith(initial): raise ILError(initial) if self.endswith(u"ん"): raise CrtShiritoriError() def creatIL(self): """次の単語用の「次の文字」を作成する""" if self.endswith(u"ー"): nextIL = self[-2:-1] else: nextIL = self[-1:] nextIL = nextIL.translate(Furigana.to_replace) return nextIL class ShiritoriRuleError(Exception): """しりとりのルール基底クラス""" def __init__(self, value=None): self.value = value class UniquenessError(ShiritoriRuleError): def __str__(self): return u"この単語は" + str(self.value) + u"番目に使いました。" class DataNotFoundError(ShiritoriRuleError): def __str__(self): return u"この単語ははてなキーワードに登録されていませんでした。" class XMLError(ShiritoriRuleError): def __str__(self): return u"xmlデータを正しく受信できませんでした。他の単語を試してください。" class ILError(ShiritoriRuleError): def __str__(self): return u"次の文字は「" + str(self.value) + u"」です!" class CrtShiritoriError(ShiritoriRuleError): """発生した場合ループを抜ける例外""" def __str__(self): return u"「ん」がついたよ" def main(): furiganadict = HatenaFuriganaDict() # API問い合わせた単語をキー読み仮名を値として管理する usedwords = {} # 正答の単語をキー、何番目に使ったかを値として管理する initial = u"は" #しりとりの最初の文字 print (u"最初のもじは「" + initial + u"」です") while True: try: wordU = input_reader(usedwords) furigana = Furigana(furiganadict[wordU]) furigana.exam(initial) except CrtShiritoriError as e: print e break except ShiritoriRuleError as e: print e continue print wordU initial = furigana.creatIL() usedwords[wordU] = len(usedwords) + 1 print u"終了です" if __name__ == "__main__": main()