見出し画像

Retro-gaming and so on

クラス入門

オブジェクト指向、と言う概念は正直良く分からない。
恐らく、僕だけじゃなく、「オブジェクト指向が分かってる」って人はほぼいない、と言って良い。

「ウソだろ?」

って思うだろう。
でも事実だ。
何故ならオブジェクト指向と言うモノの正確な定義がないから、だ。
こう書くと、

「いや、俺は知ってる。オブジェクト指向とはデータと関数をひとまとめにして作り、それを継承してプログラミングする事なんだ。」

とか言い出すあわてんぼさんが山ほど現れるだろう。
しかし、実際問題、それはオブジェクト指向、と言われるプログラミング言語が実装してる一つの側面に過ぎないのだ。
ポピュラーなオブジェクト指向の言語であるC++やJavaがそうなだけ、である。
しかし、例えばANSI Common Lispで実装されているCLOS(Common Lisp Object System)だとデータに関数は属していない。全く別なモノだし、それでもオブジェクト指向だと主張している。JavaScriptのオブジェクト指向もプロトタイプ型オブジェクト指向、と呼ばれており、名前に反してJavaのオブジェクト指向とは趣が異なる。
一体何だろうな、ってのがオブジェクト指向であり、こんなモンをプログラミングを始めて間もない人たちが使えるわけがないのは当たり前田のクラッカーなのである。

もう一回繰り返そう。
オブジェクト指向にはハッキリとした定義がない。
しかしながらオブジェクト指向を謳ってるプログラミング言語は現実に山程存在する。
そこで、ここでは基本的にはPythonを用いて、「オブジェクト指向を説明する」のではなく、具体的で簡単な「クラス」と言う機能の使い方を説明しようと思う。
そのほうが紛れが少ないと思われるからだ。
以下はC言語、ないしはPascalの経験がない人向けの「クラス入門」である。

ところで、殆どのプログラミング言語においては配列、と言うものが存在する。
Pythonには配列が無いが、代替手段としてリストが存在する。
そこで、まずはリストから話をはじめよう。

ここで例えば人・・・personと言うデータを作りたい、とする。
話を簡単にするため、人は名前(name)と年齢(age)と性別(gender)で決定される事としよう。もちろんそれ以外を考慮しても良いが、あくまで話を簡単にするため、である。
そこでリストを使って次のようなデータを定義してみる。

>>> person = ['name', 'age', 'gender']
>>> person
['name', 'age', 'gender']
>>>

まぁこうなるわな。データとしては性交もとい成功してる。
ところが、これだと一般的ではあるけれども具体性は全くない。何と言う名前なのか、年はいくつなのか、性別は何なのか、はサッパリ分からないわけだ。
じゃあ、逆にこういう順序でデータを格納する、と決めておいて、具体的なデータ例を挙げてみよう。

>>> person = ['北野未奈', 21, '女性']
>>> person
['北野未奈', 21, '女性']
>>>

今度は具体的なデータである。北野未奈っつーのは現在、FANZAで一番人気のAV女優らしいがそんな事はどーでも良い(笑)。取り敢えずは具体的なデータってぇんで拾ってきた。
上のリスト、personは確かに具体的なデータを抱えている。
しかし、今度はアクセスに難があるのは容易に想像がつくだろう。名前にアクセスするにはリストの0番目、年齢にアクセスするにはリストの1番目、そして性別にアクセスするにはリストの2番目、と一々覚えておいてアクセスせねばならない。

>>> person[0]
'北野未奈'
>>> person[1]
21
>>> person[2]
'女性'
>>>

つまり、一般性は喪われたわけだ。
じゃあ、タグとしてデータ名をくっつけて格納するのはどうか。
これはこれで上手い手ではある。

person = [['name', '北野未奈'], ['age', 21], ['gender', '女性']]

しかし、これだと依然として「リストの要素番号」でアクセスしないとならない。

>>> person[0]
['name', '北野未奈']
>>> person[1]
['age', 21]
>>> person[2]
['gender', '女性']
>>>

これだとさすがにメンド臭いので、アクセサ関数を定義する。

def assoc(key, alist):
 for item in alist:
  if item[0] == key:
   return item

こういった関数を定義すれば、

>>> assoc("name", person)
['name', '北野未奈']
>>> assoc("age", person)
['age', 21]
>>> assoc("gender", person)
['gender', '女性']
>>>

と必要なデータにアクセス可能になる。
しかし、じゃあ、別のデータを作りたい時はどうするんだろうか。また別にリストを使ってわざわざデータを構築せなアカンのだろうか。
当然、「データ作成関数」を定義すれば良い。例えば以下のようなデータ作成関数を定義してみる。

def make_person(name, age, gender):
 table = ['name', 'age', 'gender']
 data = [name, age, gender]
 return list(zip(table, data))

そうすれば新しいAV女優のデータ・・・別にAV女優じゃなくてもいいんだが(笑)、を作成する事が可能だ。

>>> person1 = make_person('高橋しょうこ', 28, '女性')
>>> person2 = make_person('河北彩花', 22, '女性')
>>> person1
[['name', '高橋しょうこ'], ['age', 28], ['gender', '女性']]
>>> person2
[['name', '河北彩花'], ['age', 22], ['gender', '女性']]
>>>

リストさえあれば、人、のデータをまとめて表現が可能だ、と言う事を見た。
・・・んで?
メンド臭くねぇ(笑)?

そう、プログラミングはただでさえメンド臭い。
メンド臭い上、またもや面倒なんてやりたくないのである。
そしてプログラミングに於いてはメンド臭さは悪、なのだ。

上で見た方式は、ぶっちゃけると、原初的なハッシュテーブル、と言うか連想リストを実装してみせたわけだが。
一々プログラムを書いてる途中でデータ生成関数やらアクセサ等を定義したくねぇのである。出来ればこの程度の事はプログラミング言語のビルトイン機能であって欲しい。
実際問題、上のようなデータを作るなら、Pythonだったら辞書型を用いれば良い。
しかし辞書型にも問題がある。
第一にあまりにも自由度が高すぎて、簡単に「名前」「年齢」「性別」以外のデータが追加可能、またはデータを削除可能だと言う事。これは利点に聞こえるかもしれないけど、「統一したデータフォーマット」を要求してる場合、ちといただけないのだ。
どっかでバグを書いちゃって、「データフォーマット」が崩れる危険性が無きにしもあらず、である。これではイカん。
もう一つの問題はアクセスの面倒臭さである。と言うのも、アクセスには文字列を要求するわけだが、一々「""」を書きたくない、と(笑)。出来れば素のリテラルでアクセスしてぇよな、ってのが人情ってモンだろ。

これを解決する為に出てきた「タグ付きの配列」、つまり平たく言うと、番号でのアクセスの代わりにリテラルでのアクセサを含んだ配列をC言語やANSI Common Lisp等の言語では構造体、Pascal等の言語ではレコード型、と呼ぶ。
そして、単純には、Pythonで構造体ないしはレコード型として使えるのがクラスなのである。

Pythonで、上で見た問題を解決する為「だけ」にクラスを使用すると、次のような定義になる。

class Person(object):
 def __init__(self, name, age, gender):
  self.name = name
  self.age = age
  self.gender = gender

これだけ、だ。これだけ覚えておけば取り敢えずは充分。
こう定義しておけば、上のようなデータ作成は次のようにして行える。

>>> person0 = Person('北野未奈', 21, '女性')
>>> person1 = Person('高橋しょうこ', 28, '女性')
>>> person2 = Person('河北彩花', 22, '女性')
>>> person0
<__main__.Person object at 0x0000011D4040F940>
>>> person1
<__main__.Person object at 0x0000011D404FF280>
>>> person2
<__main__.Person object at 0x0000011D404FF190>
>>>

具体的なデータ作成方法は基本的に上のリストを使ったデータ作成関数と変わらない。
ただし、データの中身はそのままでは見る事は叶わない。なんか面白いと言うか恐ろしげな「データ構造」が表示される。
各データの「中身」は定義されてるアクセサで次のようにして引っ張り出す事が可能だ。

>>> person0.name
'北野未奈'
>>> person1.name
'高橋しょうこ'
>>> person2.name
'河北彩花'
>>> person0.age
21
>>> person1.age
28
>>> person2.age
22
>>> person0.gender
'女性'
>>> person1.gender
'女性'
>>> person2.gender
'女性'
>>>

簡単でしょ?
先に見たような「データ生成関数」及び「アクセサ」を定義しなくても自在に「まとまったデータ」を扱う事ができる。
これが構造体・・・と言うより、まずはPythonのクラスの強みであり、また、基本的な使い方なのだ。
まずはこれを押さえよう。

ところでもう一人くらいAV女優を追加してみようか。

person3 = Person("蓮実クレア", 29, "女性")

もうホントどーでも良い話であるが、彼女の誕生日は12月3日らしい。
この記事を書いてる最中は10月23日である。
つまり、あと二ヶ月しねぇウチに彼女は30歳、と言う大台に乗るんだな。
いや、ホンマどーでもいいんだが(笑)。
今、彼女の年齢データは次のようになっている。

>>> person3.age
29
>>>

これがあと二ヶ月しねぇウチに30に更新される。
従って、二ヶ月後には次のようにして代入してデータを変更しなければならない。

>>> person3.age = 30
>>> person3.age
30
>>>

分かるだろうか?データが書き換えられている。つまりデータを破壊的変更したわけだ。
ハッキリ言うと、構造体、ないしはオブジェクト指向と言う手法は関数型プログラミングとは相性が悪い。前提として破壊的変更を用いるプログラミングになるのが避けられないから、だ。
僕個人の思い出でも、Schemeを学んで曲りなりにも関数型プログラミングの手法が分かって、さぁ、ANSI Common Lispをやってみるか、となった時、構造体の出現で、

「一体何だ、この構造体の気持ち悪さは」

と感じた事がある。構造体はconsではないのだ。データに必ず破壊的変更を伴う、と言うのに怖気づいたのだ。

例えばANSI Common LIspだと次のようにして構造体を定義する。

(defstruct person
 name
 age
 gender)

(defparameter hasumi (make-person :name "蓮実クレア" :age 29 :gender "女性"))

今から二ヶ月後くらいで蓮実クレアが30になれば次のようにして年齢を変更するしかない。

CL-USER> hasumi
#S(PERSON :NAME "蓮実クレア" :AGE 29 :GENDER "女性")
CL-USER> (setf (person-age hasumi) 30)
30
CL-USER> hasumi
#S(PERSON :NAME "蓮実クレア" :AGE 30 :GENDER "女性")
CL-USER>

Pythonのクラスに比べると中に入ってるデータは丸見えなんである意味安心ではあるのだが、どっちにせよ、Lispらしくなく(笑)、setfによる破壊的変更が基本である。
もちろん、「頑張れば」関数型プログラミングっぽくは出来るだろう。

CL-USER> (let ((p0 hasumi))
      (defparameter younger-hasumi (make-person :name (person-name p0)
                          :age 29
                          :gender (person-gender p0))))
YOUNGER-HASUMI
CL-USER> younger-hasumi
#S(PERSON :NAME "蓮実クレア" :AGE 29 :GENDER "女性")
CL-USER>

要するに元データから値をコピーして変更したいトコだけ新しい値を入れた新しいデータを作成するわけである。
しかし、いくら何でもこれだと煩雑過ぎるだろう。

結局、構造体、と言う考え方そのものが、元々手続き型言語のものであり、これに関しては「破壊的変更を辞さず」と言った覚悟をもって扱った方がベターだ、と言う事だ(※1)。

さて、Pythonに話を戻す(とは言ってもANSI Common Lispでも変わらんが)。
最初のように「人」をリストで表現した場合、データ型はリストだった。
一方、クラスで定義した場合、そのデータ型は一体何だろうか。
クラス?
実は違う。
person0typeを適用してみよう。

>>> type(person0)
<class '__main__.Person'>
>>>

何かこんなん出てきたんですけど〜。
そう、実はperson0はPerson型、になってるのだ。
すなわち、

>>> isinstance(person3, Person)
True
>>>

これ、ピンと来ただろうか。
初歩のPythonプログラムだと、あまり型を意識はしない。何故なら動的型付けプログラミング言語だからだ。
そして基本的な「型」は全てPython組み込みであり、例えばそれらは整数であったり、浮動小数点数だったり、あるいは文字列型だったりした。
しかし、今、Personをクラスとして定義した為に、Personはリスト等の組み込み型ではなくPerson型、となったのだ。
言い換えると、我々はクラスによって型自体を作成出来る方法を手にした事になる。
そう、実はクラス作成の本質は何かと言うとユーザー定義型作成なのだ。
つまり、ここに来て、我々は「ユーザー定義関数」作成方法の他に「ユーザー定義型」の作成方法を手にした、って事なんだ。
大事な事なんでもう一度書く。
Pythonではクラス作成とはユーザー定義型作成の事なのだ。
関数を作れるなら型も作れていいじゃない、ってのがその本質なのだ。

ここをまずは押さえておこう。
実の事を言うと、ここを押さえずにいきなり「オブジェクト指向では〜」とかはじめるからプログラミング初心者が混乱するのである。だから俺は混乱した(爆
畜生。

ところで我々は変数を学んだ。
例えば変数には「整数型」の何かを代入出来、その場合、変数は当然「整数型」と言う性質を持ちながら別々に定義出来る。

>>> a = 1
>>> b = 2
>>> a
1
>>> b
2
>>>

当然、変数にはリストが代入出来る。この場合も全く別々のリストを別の変数に、「リスト型」と言う性質を持たせつつ代入が可能だ。

>>> c = [1, 2, 3]
>>> d = [4, 5, 6]
>>> c
[1, 2, 3]
>>> d
[4, 5, 6]
>>>

つまり、クラス作成はユーザー定義型作成だ、と言う事は上と同じような事が出来る、と言う事を意味する。
って事はだ。
次の画面を見て欲しい。


要するにこういう事である(※2)。

class Brave(object):
 def __init__(self, strength, dexerity, maxHP, maxMP, THAC0 = 0, AC = 0, equip =[], EX = 0, name = "", LV = 1):
  self.strength = strength
  self.dexerity = dexerity
  self.maxHP = maxHP
  self.maxMP = maxMP
  self.THAC0 = THAC0
  self.AC = AC
  self.HP = maxHP
  self.MP = maxMP
  self.equip = []
  self.EX = EX
  self.name = name
  self.LV = LV

つまり、ドラクエの勇者はPython上では「勇者型」と言う型で定義出来る(※3)。
従って、ローレシアの王子はこうであり、

The_Prince = Brave(5, 4, 28, 0)

サマルトリアの王子はこうであり、

The_Prince_of_Cannock = Brave(4, 4, 31, 6)

そしてムーンブルクの王女はこうである。

The_Princess_of_Moonbrooke = Brave(2, 22, 32, 28)

こんな事が可能だ、って事になってくると途端にクラスに愛着が沸いてこないか(笑)?
もう一回言うと、クラスで勇者型が定義出来る、と言う事は勇者型でドラクエのキャラクタを表現可能だ、って事だ。
同様に、クラスを使えば

class Monster(object):
 def __init__(self, HP, MP, THAC0, AC, dexerity, EX, gold, name):
  self.HP = HP
  self.MP = MP
  self.THAC0 = THAC0
  self.AC = AC
  self.dexerity = dexerity
  self.EX = EX
  self.gold = gold
  self.name = name

と言うカタチでモンスター型を定義出来る(※4)。
従って、

Slime = Monster(5, 0, 7, 5, 2, 1, 2, "スライム")
Big_Slug = Monster(8, 0, 9, 6, 3, 2, 3, "おおなめくじ")
Iron_Ant = Monster(5, 0, 11, 13, 4, 2, 4, "アイアンアント")
Drakee = Monster(9, 0, 12, 8, 5, 3, 3, "ドラキー")


と、スライム、おおなめくじ、アイアンアント、ドラキー、と定義が可能だ(※4)。

ここで、代表者として勇者側からはローレシアの王子、モンスター側からはスライムを選んで両者にガチンコ対決をしてもらおう。ドラクエIIの序盤だな。
「ガチンコ対決」つったって、どうすんだ?とか思うかもしれない。
しかしクラス定義はユーザー型定義でも単なる型なんで、別にフツーに定義した関数で戦いを演出するこたぁ可能である。
そこで、このサイトによるドラクエIIでの攻撃のダメージ計算式を借りてこよう。

基本攻撃ダメージ=(攻撃力-敵の守備力÷2)*乱数値(54~197)/256

実際はこんなに簡単じゃないみたいだが、単純なのでこれを採用する。
そうすると、

from random import randint
def attack2enemy(member, enemy):
 return round((member.THAC0 - enemy.AC / 2) * randint(54, 197) / 256)

と言うのがダメージの計算式となる。
そして、上の関数を利用して次のような関数を書いてみる。



さて、さすがのローレシアの王子でも徒手空拳ではツラい。
そこで、取り敢えず、初期装備の「どうのつるぎ」と「かわのよろい」の攻撃力と防御力を与えておこうか。
ついでに名前を「えにくす」にしとく(笑)。

>>> The_Prince.THAC0 = 10
>>> The_Prince.AC = 6
>>> The_Prince.name = "えにくす"

関数head_on_fightにローレシアの王子とスライムを与えてさぁ、ガチンコ勝負だ!

>>> head_on_fight(The_Prince, Slime)
スライムがあらわれた!
えにくすのこうげき!
スライムに2ポイントのダメージをあたえた!
スライムのこうげき!
えにくすは2ポイントのダメージをうけた!
えにくすのこうげき!
スライムに4ポイントのダメージをあたえた!
スライムをやっつけた!
けいけんち1ポイントかくとく
2ゴールドをてにいれた。

うん、上手く回ってるね。
今んトコ、ゴールド保有の為の変数は決めてないないんで、実際はゴールドは入手してないわけだけど(パーティの共有資産な為、今んトコ「一人」に対しては実装してない)、経験値はキチンと入手が出来ている。

>>> The_Prince.HP
25
>>> The_Prince.EX
1
>>>

まぁ、今のトコ、この戦闘なんかは単純なドラクエII戦闘シミュレータなんだけど、当面の目的は果たしてくれた、と言う事は分かるだろう。
そして、ユーザー定義型の時、データ内容にアクセスするのに変数名.データと言う形式でアクセスしますよ、と言う事以外はフツーに関数に渡せて、計算の対象に出来る、って事が分かった筈だ。
そう、クラスを構造体として使う、って前提だと特に難しくも何ともないのだ。
まずは最初のウチは、ユーザー定義データ型、と言うカタチに慣れよう。そして自分が定義したデータ型に自分が定義した関数を適用させよう
それがいけない、と言うルールはない。
そして、それに慣れきった後、いわゆるオブジェクト指向に入る準備が出来てる筈である(※6)。
いわゆるオブジェクト指向の残りはメソッド、と言う関数代わりの概念しか残されていないのだから。

※1: これが原因かどうかは知らないが、結果、長い間Schemeの仕様には構造体は含まれていなかった。
もっとも、SRFI-9及びSRFI-57にはレコード型、として構造体が提案されてきてて、R6RSからレコード型が正式に導入された。
またRacketでは組み込みのstructと言う名前で構造体が提供されている。
※2: ここでは「こうげき力」にTHAC0、「しゅび力」にACと言った単語を当てはめたが、TRPGマニアに言わせればこれらは厳密な用法ではない。
ただ、比較的短く変数名として使えるのでTRPGから借用してきただけだ。
※3: 本当は魔法のリストも必要だろう。
※4: 本格的なオブジェクト指向だと、RPGはパラメータ込みのキャラとして勇者側もモンスター側も設計されているので、共通パラメータを持つ「親クラス」を作っておいて、勇者型もモンスター型もその「親クラス」を継承して作成する、と言う手順を踏むだろうが、ここでは割愛する。
※5: 本来だったらモンスターの種族自体もモンスター型を継承してスライム型、おおなめくじ型、アイアンアント型、ドラキー型、と作るべきだろう。その方がスライムA、スライムB、と複数体を作りやすいから、だ。
※6: Cの習得者がC++に於けるオブジェクト指向に混乱しない、あるいはPascalの習得者がObject Pascalに混乱しない最大の理由はこれ、である。
少なくとも、C++やObject Pascalが提供するところに於ける「オブジェクト指向」は単純に言うと、Cに於ける構造体、ないしはPascalに於けるレコード型の拡張に過ぎなく、結果、構造体/レコード型を使いまくってたC/Pascalユーザーにとっては「より便利になった構造体/レコード型」を使えるようになった、に過ぎないから大して悩まなくて済むわけだ。
一方、Javaは言語として「全部をクラスとして書かなければならない」と言うとんでもない設計を持ち出してきて(笑)、その結果「オブジェクトは現実世界の物体を表現する手段」等と分かったような分からないような説明をするハメになったのだ。
Pythonは何度か言ったがオブジェクト指向プログラミングを強制しないので、本来ならここで書いたように、クラス作成をユーザー定義型作成と限定し、New Comerにはフツーにプログラムを書かせる事を主眼とさせるべきである。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事