Pythonしかわからん初心者でも理解できる抽象クラス(手を動かしながらABCモジュールを理解する)
「良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方」という技術書を読んでいて、インターフェースについてPythonだとどうやって実装するのか気になったので、ドキュメントを読みつつ理解してみた
(この本はJavaを前提としているが、自分はJava分からないので、ここではPythonでやってみる)
間違ってたらごめんなさい
良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方
PythonはVer3.9を利用しています
% python -V Python 3.9.1
Pythonでインターフェースを実現するにはABC(抽象基底クラス)を使う
まず、オブジェクト指向プログラミングでの大大大前提として、頭に入れておきたいのは、「より”縛り”のあるプログラムの方が良い」ということ
自分は、最初ここが抜けていたので理解に苦しんだ
日本語の文章を書くにしても、「さあ、自由に書いてください」と言われるより、「”誰が、どこで、何を、どうした”という形式で書いてください」と言われる方が書きやすい
ここでいう(オブジェクト指向プログラミング)での「インターフェース」とは、そういったプログラムの「型」を定義する手法の一つであると理解した
そして、Javaと違って、Pythonには「インターフェース」という概念が基本的には無い
しかし、「ABC(abstract base class=抽象基底クラス)」というモジュールを使って、似たようなことが実現できる
なので、本記事では、ABCの公式ドキュメントを読みながら、使い方について理解していく
正直、Pythonからプログラミングを始めて、インターフェースなんて概念を知らなかった自分にとっては、公式ドキュメントを読むだけでは理解が難しく、他のブログ記事も読み漁ってやっと、なんとなく理解できた
初学者にとって分かりやすい記事が少ないと思ったので、この記事では、Pythonから入った初学者でも理解できるように易しくまとめてみたつもりです、、
(具体的に参考にした記事は、以下「参考リンク」に後述)
概要(ABCを使って嬉しいこと)
まずは、今回書いたコードの全体像についてざっくり説明
一部簡略化しているが、UMLにすると以下
CarクラスはABC(抽象基底クラス)で、Firetruckクラス、PoliceCarクラス、Busクラスが継承している
(「FiretruckとPoliceCarとBusは、Carの子分なんだな」と理解すればOK)
Carクラスでは、「全員、nameメソッドは定義しろよ」と指定しているので、Carクラスを継承しているクラスたちは、nameメソッドを定義しなければならない
nameメソッドさえ定義していれば、その他は独自のメソッド(ここでは、PoliceCarクラスのsirenメソッドや、Busクラスのdriveメソッド)を自由に追加してOK
Carクラスそのものは機能を持たず、子分のクラスたちに指示をするだけ
そして、ABCの何が一番嬉しいかと言うと、”機能の実装し忘れを防げること”
上の例で言うと、
子分クラスでnameメソッドを実装し忘れると、インスタンス化する時にエラーが発生するので、気づくことができる
また、「FiretruckとPoliceCarとBusクラスは、同じCarクラスの一員なんだな」と一目でわかり、コードがわかりやすくなる
他にも色々メリットあるかもしれないが、自分が思ったメリットはこのくらい
ABCの作り方(メタクラスとは?)
では、具体的な書き方について見ていく
ABCを使うには、「ABCMeta」というクラスを「メタクラス」として呼び出す必要がある
「じゃあ、”メタクラス”とはなんぞや」だが、メタクラスとは「クラスの振る舞いを決めるクラス」である
言い換えると、「クラスを定義するときに呼び出されるクラス」と理解した方が分かりやすい
参考:メタクラス
そもそも、Pythonでクラスを定義するときは、裏で「type 」という組み込みクラスが呼び出されている
皆さんがよく変数の型を調べるときに使うやつであるが、typeには、実は、変数の型を調べる他にクラスを定義する機能があった
※ typeで変数の型を調べる時の使い方
>>> type("hoge") <class 'str'>
※ typeでクラスを定義する時の使い方
>>> X = type("X", (object,), dict(a=1)) >>> X.a 1 >>>
参考:組み込み関数-type
すなわち、Pythonでは、デフォルトではtypeがメタクラスとなる
で、今回は、ABCを作るときに、メタクラスとして、デフォルトのtypeではなく「ABCMeta」というクラスをメタクラスとして呼び出す事になる
(ドキュメントに書いてある通り、ABCを定義する時はABCMetaを使うように決まっている)
※ ABCソースコードの該当箇所
メタクラスを指定するには「metaclass=【指定したいメタクラス】」と記述するので、ABCを定義する(=ABCMetaをメタクラスとして指定する)には、以下のように記載する
from abc import ABCMeta class MyABC(metaclass=ABCMeta): ...何か処理...
もう少しシンプルな書き方として、以下のように記載することもできる
from abc import ABC class MyABC(ABC): ...何か処理...
abstractメソッドの作り方と使い方
概要部分 で書いた通り、ABCは、子分クラスたち(継承されるクラスたち)でどんな機能を実装すべきか指示をする
指示をするためには、abstractmethodデコレータを使う
基本的にABCそのものは意味を持たず、子分クラスで定義するべきメソッドを指示するだけなので、ABCのabstractメソッドの中身はpassとだけ書いておいて、具体的な処理は子分クラスのメソッドで定義する
from abc import ABC from abc import abstractmethod class Car(ABC): @abstractmethod def name(self): pass
これで、ABCは完成したので、このCarクラスを継承する子分クラスたちを作っていく
Carを継承するFiretruckクラスとPoliceCarクラスは、必ずCarでabstractメソッドとして定義したnameメソッドを持つようにする
PoliceCarのsirenメソッドのように、メソッドを追加するのはご自由にOK
class Firetruck(Car): def name(self): return "I'm a firetruck" class PoliceCar(Car): def name(self): return "I'm a police car" def siren(self): return '"woo woo woo"'
例えば、以下のようにCarクラスを継承しているのにnameメソッドを定義していないと、インスタンス化しようとしたタイミングでエラーになる
このエラーがあるお陰で、nameメソッドの実装し忘れが回避できるという、ABCの恩恵を享受することができる
class Ambulance(Car): def siren(self): return '"nee-naw nee-naw"' ambulance = Ambulance()
※ エラー文
% python abc_test.py Traceback (most recent call last): File "~/abc_test.py", line XX, in <module> ambulance = Ambulance() TypeError: Can't instantiate abstract class Ambulance with abstract method name
ここまでのコード全体は以下
% cat abc_test.py from abc import ABC from abc import abstractmethod class Car(ABC): @abstractmethod def name(self): pass class Firetruck(Car): def name(self): return "I'm a firetruck" class PoliceCar(Car): def name(self): return "I'm a police car" def siren(self): return '"woo woo woo"' class Ambulance(Car): def siren(self): return '"nee-naw nee-naw"' print("###FIRETRUCK###") firetruck = Firetruck() print(firetruck.name()) print("###POLICECAR###") policecar = PoliceCar() print(policecar.name()) print(policecar.siren()) # ambulance = Ambulance() # TypeError
※ 実行結果
% python abc_test.py ###FIRETRUCK### I'm a firetruck ###POLICECAR### I'm a police car "woo woo woo"
registerの使い方と注意点
ここまでの話で、ABCの基本的な使い方は網羅できているのだが、ドキュメントを読むと、「register」という機能も気になったので、動かしてみた
registerは、「上で書いたコードのようにわざわざ継承させなくても、子分クラスを作れるよ」という機能と理解
こう書いていたものを
% cat abc_test.py from abc import ABC from abc import abstractmethod class Car(ABC): @abstractmethod def name(self): pass class Firetruck(Car): def name(self): return "I'm a firetruck" print("###FIRETRUCK###") firetruck = Firetruck() print(firetruck.name())
※ 実行結果
% python abc_test.py ###FIRETRUCK### I'm a firetruck
こう書ける
% cat abc_test.py from abc import ABC from abc import abstractmethod class Car(ABC): @abstractmethod def name(self): pass class Firetruck(): # Carを継承しない def name(self): return "I'm a firetruck" print("###FIRETRUCK###") Car.register(Firetruck) # ここでRegister firetruck = Firetruck() print(firetruck.name())
※ 実行結果
% python abc_test.py ###FIRETRUCK### I'm a firetruck
結論から言うと、registerを使う意味があまりわからなかった
registerで定義すると、issubclassなどで確認すると確かに継承されているのだが、
nameメソッドを定義しなかった場合にエラーが出ないので、ABCのメリットを享受できなさそうと思った(この辺りはもう少し勉強が必要かも、、)
※ サブクラス調査
% cat abc_test.py from abc import ABC from abc import abstractmethod class Car(ABC): @abstractmethod def name(self): pass class Firetruck(): def name(self): return "I'm a firetruck" Car.register(Firetruck) firetruck = Firetruck() print(f"is subclass: {issubclass(firetruck.__class__, Car)}")
※ 実行結果
% python abc_test.py is subclass: True
以上、拙い文章失礼しました。