じゃんけんプログラムと演算子オーバーロード
複数人でじゃんけんを続け、勝敗を記録するプログラムを作ってみた。練習用のプログラムでよくあるやつっすな。
実用性ねぇ
その際、グー、チョキ、パーとその勝敗をどう表そうか若干悩む。
C言語の列挙体みたいなのを使えればわかりやすそうだが、Pythonだと列挙体なさそう(最新版だとあるみたい?)だから、
とりあえず文字列'g' 'c' 'p'をそのまま使えばいいだろうとなった。
勝敗判定は、条件分岐をいちいち全部書いてたらだるそうだから、
辞書{'g':'c', 'c':'p', 'p':'g'}を定義することで行数削減。
その方針でコードを書き進めていたが、途中で演算子オーバーロードが使えるのでは? と気づく。
要するに、グー>チョキ、チョキ>パー、パー>グー みたいに書きたい。
A > B かつ B > C のときに A > Cが成り立っていない不等号なので、こういう定義の仕方は微妙かもしれんが。
というわけで、文字列の大小関係を定義しなおせばいいんだな。しめしめ、とテストコードを書き始める。
class String: def __gt__(self, x): # overload > return self < x print( 'a' > 'b' ) # 出力 False
期待通りに動かない。残当。
これでは既存のStringクラスのメソッドを書き換えようとしているので、
演算子オーバーロードでなく、そしてオーバーライドですらない(オーバーライドとは派生クラスのメソッドを再定義するものだから)。
ただただ危険な行為だから、そんなことはPythonでは最初からできないようにしているようだ。優しい。
一方、Rubyでは
class String def >(x) self < x end end print 'a' > 'b' # 出力 true
trueになったし! 元のメソッド書き換えちまったし! こわwwwww
いや、それとももっと上位のクラスでメソッド>()が定義されていて、それをStringクラスで上書きしているだけなのか・・・?
それならこういう挙動になるのは理解できる。
まあ、Rubyの話はおいといて、Pythonでは既存のStringクラスのメソッドは書き換えられないので、
新たにJankenクラスを定義し、Jankenオブジェクト同士の不等号を定義することにする。
これなら問題はない。
import random class Janken: def __init__(self, gcp): # gcp in ['g','c','p'] self.gcp = gcp def __gt__(self, janken): # overload > tmp = {'g':'c', 'c':'p', 'p':'g'} return janken.gcp == tmp[self.gcp] class Human: def __init__(self, name): self.name = name self.win = 0 self.lose = 0 self.same = 0 self.mylist = [] def janken(self): tmp = [Janken('g'), Janken('c'), Janken('p')] n = random.randint(0, 2) self.mylist.append(tmp[n].gcp) return tmp[n] def show_result(self): print (self.mylist) print (self.win, self.lose, self.same) class Taikai: def __init__(self, *human): self.human_list = list(human) def janken(self, i, j): # human_i vs human_j def win(human1, human2): human1.win += 1 human2.lose += 1 def draw(human1, human2): human1.same += 1 human2.same += 1 human_i = self.human_list[i] human_j = self.human_list[j] comp_i = human_i.janken() comp_j = human_j.janken() if comp_i > comp_j: win(human_i, human_j) elif comp_j > comp_i: win(human_j, human_i) else: draw(human_i, human_j) taro = Human('Taro') jiro = Human('Jiro') taikai_1 = Taikai(taro, jiro) for i in range(24): taikai_1.janken(0, 1) taro.show_result() jiro.show_result() # 出力 # ['g', 'g', 'c', 'p', 'p', 'g', 'c', 'p', 'c', 'p', 'p', 'c', 'g', 'g', 'c', 'g', 'c', 'p', 'c', 'p', 'c', 'p', 'c', 'c'] # 8 6 10 # ['p', 'c', 'c', 'p', 'c', 'c', 'c', 'p', 'c', 'p', 'g', 'c', 'g', 'p', 'p', 'c', 'c', 'g', 'p', 'c', 'g', 'c', 'p', 'c'] # 6 8 10
変数・クラス・メソッド名があまりにもお粗末だったり、直接フィールド弄ってたりと色々よろしくない部分が多いが、
その問題はいったん見なかったことにする。
__eq__()も定義しようか迷ったが、元々Jankenクラスのオブジェクトに適用される、存在するわけでもない>記号を「新たに定義」するのと違って、
元から==演算子はJankenクラスのオブジェクトに対して適用できる。
これを上書き(? 多重定義? どっちだ?)しちゃうのは流石に危険だなーと思ってやめた。
こうして改めてコードを書いてみると、演算子オーバーロードとは何なのか、よくわかる。
オーバーライドとオーバーロードの違いは簡単だ。派生クラスで元のクラスのメソッドを上書きするか、同じクラスで同名のメソッドを複数定義するかの違いに過ぎない。
だが、「演算子オーバーロード」という言葉には昔から引っ掛かりしかなかった。
なぜ「オーバーライド」でないのか疑問に思っていたが、今回謎が若干解けたかもしれない。
例えば、Point(1, 3) + Point(10, 20) = Point(11, 23) みたいに「新しい」演算子+を定義するというお話だが、
ここで重要なのは「新しい」というところだ。
オーバーライドは上書きのことなので、上書きされる前の元のメソッドがある。
Pointクラスがどのクラスを継承しているのか知らないが(もしかしたらトップレベルを継承しているのかもしれないが)、
その派生元クラスに上書きされる前の元のメソッドがあるということになってしまう。
そういう話ではないだろう。
、とこう考えた後、悩むわけですよ。
例えば、実数クラスを継承して複素数クラスをつくるって話なら、実数クラスの演算・メソッドを継承して複素数の演算をつくるという話だっておかしくはない、少なくとも数学では。
Pointクラスだって、抽象ベクトル空間インターフェースを継承して加算演算を実装したという解釈だってできる。少なくとも数学では。
実際、Haskellの型クラスとかはそういう考えに立脚してるように思えるし。
あと、Jankenクラスに==演算子(__eq__()メソッド)を定義できるのは一体何なのかと。
これは多重定義というべきなのか、それとも上書きなのかと。
元から==演算子をJankenクラスのオブジェクトに適用できるので、全く新しい演算子とは言い難く、挙動だけ見ると上書き? と言いたくなる。
それに、トップレベルでの
def 既存の+(<Type1> 引数群) {} def 新しい+(<Type2> 引数群) {} と、 def +(<Interface Addable>引数群) {引数1.add(引数2)}
って結局同じじゃない? っていう疑問にも辿り着く。
ああ、わからない、わからない。演算子オーバーロードがわからない。
そもそも演算子オーバーロードというのはC++での概念で、他の言語でいうところの演算子オーバーロードなんて、
本家演算子オーバーロードに似てるからそう呼んでいるだけの俗称、と言われれば確かにそうだ。
動的型付けでオーバーロードとかそんなのあるのかって話だし(あるのかもしれない)。
結局、少なくともC++ではオーバーロードなんだから、そこは疑問の余地ないだろうという結論になる。
あとC++勉強しろという結論になる。