PythonにおけるProtocol(ダックタイピング)とABC(抽象化)の違い
Qiita 「Python Advent Calendar 2022」 18日目の記事です。
Pythonにおけるポリモーフィズムの実装で、「ABC(Abstract Base Class/抽象基底クラス)とProtocol(プロトコルクラス)の違いってなんだ?」と疑問に思ったので、実際に試して確認してみました。
ABCとProtocolとは何か?
Pythonの公式ドキュメント
使いどころ
ABCとProtocolのつかいどころを、ざっくり書くと下記のようになります。
- ABC
- CやJavaで言うところの、抽象クラスを作る時に使う
- Protocol
- ダックタイピングをするときの型定義に使う
何が違うか?
ABCもProtocolもポリモーフィズムを実現する為に使いますが、微妙に考え方が違います。
例えば、人間
と絵を描く
という関係を例に違いを見てみます。
- ABCを使う場合は、次のように考えます
-
人間
であるならば、絵を描く
ことができる
-
- Protocolを使う場合は、次のように考えます
-
絵を描く
ことができるならば、人間
である
-
細かい話を書くと混乱してくるので、実際にコードを書いてみましょう。
コードを書いてみた
実際に書いたもの
単純なコードですが、一応GitHubにも置いておきます。
前提
poetryを使っています。
エディタはVSCodeを使い、型チェックのためにmypyをインストールしておきます。
poetry add mypy
状況設定
前述の人間
と絵を描く
の関係に対して、下記の4人を設定します。
- 田中(Tanaka)
- 絵が描ける人間
- 山田(Yamada)
- 絵が描ける人間
- 鈴木(Suzuki)
- 絵が描けない人間
- AI
- 絵が描けるが、実は人間ではない
ABCを試す
- 必要なライブラリをimportします。
from abc import ABC, abstractmethod
- 人間を表す抽象クラス
Human
を作ります。- 抽象基底クラス
ABC
を継承して定義します。 - 絵を描く
draw
メソッドを定義します。@abstractmethod
をつけたメソッドは抽象メソッドになります。中身は定義しないのでpass
します。
- 抽象基底クラス
class Human(ABC):
@abstractmethod
def draw(self) -> None:
pass
- 絵が描ける人間の田中と山田を表すクラスを作ります。
- さきほど定義した
Human
を継承して、draw
メソッドの中身を書きます。
- さきほど定義した
class Tanaka(Human):
def draw(self) -> None:
print("😁")
class Yamada(Human):
def draw(self) -> None:
print("👶")
- 絵が描けない人間の鈴木を表すクラスを作ります。
- 絵が描けないので、
draw
ではなく適当に別なメソッドを定義しておきます。
- 絵が描けないので、
class Suzuki(Human):
def write(self) -> None:
print("こんにちは")
- 絵が描けるAIを表すクラスを作ります。
- 人間ではないので、
Human
は継承しません。 - 絵が描けるので、
draw
メソッドを定義します。
- 人間ではないので、
class AI:
def draw(self) -> None:
print("👽")
- 人間が絵を描く関数を定義します。
def run_draw(human: Human) -> None:
human.draw()
- 田中、山田、鈴木、AIのインスタンスを作ります。
tanaka: Tanaka = Tanaka()
yamada: Yamada = Yamada()
suzuki: Suzuki = Suzuki()
ai: AI = AI()
ここで、VSCodeに怒られます。
鈴木に対して怒っていますね。
「人間
であるならば絵を描く
ことができる」という前提がありますので、
「人間
なのに絵を描く
ことができない」鈴木は存在できません。
残念ですが消えてもらいましょう。
tanaka: Tanaka = Tanaka()
yamada: Yamada = Yamada()
# suzuki: Suzuki = Suzuki()
ai: AI = AI()
- 生き残った田中と山田とAIに絵を描かせましょう。
run_draw(tanaka)
run_draw(yamada)
run_draw(ai)
ここで、VSCodeに怒られます。
AIに対して怒っていますね。
run_draw
関数は「人間が絵を描く関数」と定義しました。
AIは絵は描けますが人間ではありませんので、お断りされてしまいます。
AIにも消えてもらいましょう。
run_draw(yamada)
run_draw(tanaka)
# run_draw(ai)
- 実行します。
% poetry run python src/abc_sample.py
😁
👶
ABCまとめ
Protocolを試す
続いて、Protocolの場合はどうなるかを試します。
- 必要なライブラリをimportします。
from typing import Protocol
- 人間を表すプロトコルクラス
Human
を作ります。-
Protocol
を継承して定義します。 - 絵を描く
draw
メソッドを定義します。中身は定義しないので...
とします。
-
class Human(Protocol):
def draw(self) -> None:
...
- 絵が描ける人間の田中と山田を表すクラスを作ります。
- ABCと違って継承は不要です。
draw
メソッドの中身を書きます。
- ABCと違って継承は不要です。
class Tanaka:
def draw(self) -> None:
print("😁")
class Yamada:
def draw(self) -> None:
print("👶")
- 絵が描けない人間の鈴木を表すクラスを作ります。
- 絵が描けないので、
draw
ではなく適当に別なメソッドを定義しておきます。
- 絵が描けないので、
class Suzuki:
def write(self) -> None:
print("こんにちは")
- 絵が描けるAIを表すクラスを作ります。
- 絵が描けるので、
draw
メソッドを定義します。
- 絵が描けるので、
class AI:
def draw(self) -> None:
print("👽")
- 人間が絵を描く関数を定義します。
def run_draw(human: Human) -> None:
human.draw()
- 田中、山田、鈴木、AIのインスタンスを作ります
tanaka: Tanaka = Tanaka()
yamada: Yamada = Yamada()
suzuki: Suzuki = Suzuki()
ai: AI = AI()
先ほどと違い、ここでは怒られません。
- 田中と山田と鈴木とAIに絵を描かせましょう。
run_draw(yamada)
run_draw(tanaka)
run_draw(suzuki)
run_draw(ai)
ここで、VSCodeに怒られます。
鈴木に対して怒っていますね。
絵が描けない鈴木には、絵を描く為の関数であるrun_draw
を実行することは許されません。
残念ですが今回も消えてもらいましょう。
run_draw(yamada)
run_draw(tanaka)
# run_draw(suzuki)
run_draw(ai)
ここで注目はAIです。ABCでは脱落したのにProtocolでは生き残っています。
「絵が描ける
なら人間
である(→本当に人間
であるかはどうでもよい)」というのがProtocolです。
このような考え方を世の中ではダックタイピングといいます。
- 実行します。
% poetry run python src/protocol_sample.py
👶
😁
👽
Protocolまとめ
ググると「ABCを使ってダックタイピングをする」系の記事も出てきますが、それは抽象化でありダックタイピングとは呼べないです。
かといって型を定義しない世界でダックタイピングをすることは、個人開発や小規模開発ならいいかもしれませんが一般的なプロダクト開発ではツライことになります。
Pythonでダックタイピングをしたいときは、Protocolを使うべきだと思います。
全体まとめ
ABCはAI絵師は許容できない派、ProtocolはAI絵師でも問題ない派
最後に
全世界の鈴木さん、ごめんなさい。ただの例示であり他意はないです。
また、絵が描けるかどうかで人を区別する意図もありません。
私は絵は描けません。
Discussion
とてもわかりやすい