#継承に関するうだうだ
文法編の最後は8章の「クラスとオブジェクト指向プログラミング」。
ここが一番読み進めるのに骨の折れたところ。
Pythonだと「ブロック単位のローカル変数」という概念がないから、Javaを見慣れた目でPythonによるクラス定義を見るとどうしても違和感があるのよね。
とはいえ、基本的にクラスシステムとして必要な道具立てはJavaでもPythonでもさほど変わらないので、そこそこマッピングはできつつある。
しかしあれな、MITの先生でも継承に関する説明だと微妙に不適切な例を使うのな...
基本的に継承を使うケースとしては「UMLによるJavaオブジェクト設計 第2版」(Peter Coad+Mark Mayfield、日本語版は1997年が初版)では以下の条件が満たされている場合に限られると書いてある。
- サブクラスが「~の特別な種類」であって「~によって果たされる役割」ではない場合
- 何か別のクラスのオブジェクトに変形する必要は生じない
- スーパークラスを拡張するのであってスーパークラスをオーバーライドまたは無効化するのではない
- ユーティリティクラスにすぎないクラスをサブクラス化するのではない
- ドメインの中では役割、トランザクションまたは物の特別な種類を表す。
もちろん、現代において最後の条件に関しては保留せざるを得ない。
デザインパターンの中に登場するオブジェクトが既にこの範疇に収まらないからである。
2番目に関しては要するに「ある時点におけるクラスAのインスタンスが別の時点ではBクラスのインスタンスにならなければいけない」という状態を許容しないということである。
具体例でいえばテキストにも出ている例だけど「ある時点で学部生であるインスタンスが別の時点では卒業生のインスタンスになる」というような時系列によって「状態」が変化するような表現をするために継承による分類を使うのは間違っているということ。
だって状態が変わるたびにインスタンス作り直すんだよ?めんどくさいに決まってるじゃないの。
下手うつとデータの整合性も保てなくなるしさ。
この辺の表現をする場合はStateパターンを使うなりEnumを使うなりしてコンポジションにもってった方がベターというのは、割といろいろなところで言及されていることなので異論をさしはさむ人はあんまりいないと思うんだけどね。
あと3番目に関してはちょっと意味が分からない人もいるだろうけど、あくまで「継承」の条件の話であってPolymorphism全般の話じゃないことには注意しておきたい。
「継承の実装をオーバーライドしない」ということは要するにスーパークラスの実装を覆い隠して全く別の振る舞いをさせないということ。つまり、共通の実装が存在しないならインターフェースなり抽象メソッドを使いなさいということである。
実際CoadとMayfieldが第2版の原書を出したのが1999年なので初版は多分もうちょい前、1995~1996年くらいなのかな。
たぶんC++でもそれまでに同様の議論はあったと思うからそれ以前からこういう議論はなされてきているのよね。
でも日本だとソフトウェア工学は「現場」()に浸透してないのでこういう知見を利用すれば簡単に解決する問題も「難問」に見えるっていうね。
#Pythonにおけるクラス定義
とりあえずPythonにおけるクラス定義のポイントとしては以下。
- スーパークラスは省略不可。特に指定しない場合はobjectクラスを指定しておけばOK
- メソッド定義する場合は第1引数に自分自身を表す引数を指定する必要がある。引数名としては慣習的にselfを使うことが多い、らしい。
- インスタンス変数はコンストラクタにあたる
__init__
関数の中で定義。 - メソッドを利用する際には第1実引数にはドット記法で表現されるインスタンスが与えられるので、実際のメソッド呼び出しでは第2引数以降を記述すればOK。もちろん、第1引数しかない場合は表面上実引数なしで呼び出すことになる。
- JavaのtoStringにあたる
__str__
メソッドを定義可能。実装しないと型と参照(?)が表示されるところも同じ。 - JavaのhashCodeにあたる
__hash__
、Javaのequalsにあたる__eq__
メソッドも定義可能。__hash__
を実装しない場合、関数idでハッシュが与えられる。__hash__
を実装する場合、オブジェクトのライフサイクルを通じて同一値を返す必要がある、のでミュータブルなオブジェクトの場合属性値からの計算でハッシュを返してはならないことになるのかな。 - 演算子のオーバーロードが可能。オーバーロード可能な演算子は「<(``__lt__``)」「<=(``__le__``)」「>(``__gt__``)」「>=(
__ge__
)」「!=(__ne__
)」「+(__add__
)」「**(__pow__
)」など。(出典)。 - クラス変数の定義はclass宣言の下、メソッドの外。staticキーワードは不要。
- クラスメソッドは定義方法は書かれてないけどたぶんいらないんだろうなー。単に関数作ればいいだけだし。
- 「8.2.1 多重継承」とか書いてあるけど別にスーパークラスが複数継承できるわけではない。たんに継承の階層が深くなる話をしているだけ。あとリスコフの置換原理はPythonでも有効。
- 情報隠蔽の方法としては
__
を先頭に記述し、末尾に記述しないことで実現する。原則privateとpublicしか存在しない。 - ん?「サブクラスがスーパークラスの隠された属性を使おうとするときにAttributeErrorというエラーが起きていることに注意しよう。...この厄介ごとを避けるため、多くのPythonプログラマは
__
の命名規則を利用しようとしない」?うーむ...これ本当なのかな。まあおれとしては属性ではなくメソッドにアクセスする習慣が染みついてるからそこら辺はあんまり問題にならないかなとは思うけど。
とりあえずこの辺を踏まえてクラス定義を作ってみるとこんな感じになるかな。
一般的な商品クラスを考えてみる。
class Item(object):
"""一般的な商品を表すクラス。"""
#インスタンス変数は__priceと__name。
#消費税の計算用に税率をクラス変数として保持
taxRate = 0.08
def __init__(self, name, price):
"""インスタンス生成時に商品名と税抜き価格を指定して初期化する"""
self.__name = name
self.__price = float(price)
def __str__(self):
"""文字列表現を返す。"""
return "{" + str(self.getName()) + ", " + str(self.getPrice()) + "}"
def __eq__(self, other):
return self.getName() == other.getName() and self.getPrice() == other.getPrice()
def getName(self):
"""商品名を返す。"""
return self.__name
def getPrice(self):
"""税抜き価格を整数値で返す"""
return int(self.__price)
def getPriceWithTax(self):
return int(self.__price*(1.0+Item.taxRate))
item = Item('リンゴ', 100)
print(item)
print('税込み価格は', item.getPriceWithTax())
これを実行するとこうなる。
{リンゴ, 100}
税込み価格は 108
とりあえず具象クラスの定義方法はここまでかな。
この章には抽象クラスや抽象メソッドの話は載ってないけどいつものように後から出てくるんだろう。
#ジェネレータ
前にもコメントでジェネレータというのは教えてもらってたけどテキストではここでようやく出てくる。
というかでもコメントで教えてもらったジェネレータ式となんか違う...?
要するに大量の要素を持ったリスト処理を行う際などにyield
キーワードを使うことでリストを複製したりして無駄なメモリ領域を確保するのではなく、効率よく処理を行うことができる、ってことらしい。
たとえばこんな感じ。
def getItems():
itemList = [Item('桃', 200), Item('ブドウ', 180), Item('梨', 150)]
for oneItem in itemList:
yield oneItem
for i in getItems():
print(i)
で、このgetItemを実行するとこんな感じ。
{桃, 200}
{ブドウ, 180}
{梨, 150}
これだといちいちリストをコピーして返すとかしてエイリアスの心配をするよりも安全だしメモリ効率もいい、って解釈でいいのかしらん。
でもこれ前にコメントでもらったジェネレータ式とはやっぱ違うな。
きっとまた後で出てくるんだろうね。
とりあえずここまでで8章終わり。
ここからテキストはどんどんアルゴリズムの話に突っ込んでいくけどたぶんここまでで出てない文法の話もしていくんだろうね。
そんなわけでもうしばらくテキストは追っかけるけど、それと並行してDjangoも見ておかないとなー。