pythonは動的型付けの言語で、型チェックがない言語となっています。そのため実行時エラーが他言語に比べると起こりやすい言語なのですが、型ヒントを用いた開発をすることで上記のケースを減らし、保守性を高めることができます。(mypyで静的型チェックをするとなおよし)
いつも型ヒントで抽象的なクラスを宣言するとき、抽象基底クラス(ABC)を用いていたのですが、python3.8にて追加されたProtocol
を用いるとよりpythonらしく記述できると感じました。今回はそんなProtocolを用いたStructural Subtypingの紹介をしていきます。
Protocolに関して詳しくはドキュメント、またPEP544を参照ください。
Structural Subtyping(構造的型付け)とは、データの「構造」に基づいて型の判断をする型システム
のことです。
簡単に言うと、オブジェクトが持つメソッドやプロパティの構造が同じであれば、異なる型であっても互換性があるとみなされる型付けの方法です。
似たような用語で「静的ダックタイピング」というものがあるのですが、自分はほぼ同じものとして扱っています
(厳密には違うようなのですが、Pythonの公式ドキュメントでも、「structural subtyping (static duck-typing)」と説明されているので同じでよい感)
ちなみに、Structural Subtypingと違い、名前に基づいて型を判断する
のをNominal Typingと言います。pythonの抽象基底クラス(ABC)は、Structural Subtypingではなくnominal typingとなります。
詳しい違いはこちらを参照ください!
では、Protocolを用いてStructural Subtypingの具体例を書いていきます。
題材としては前回記事で書いた最後のコード部分をProtocolにしてみます。
from typing import List, Protocol class PlayableCharacter(Protocol): def full_healing(self) -> None: pass class Hero: _MAX_HP = 100 _MAX_MP = 100 def __init__(self, hp: int, mp: int, strength: int) -> None: self._hp = hp self._mp = mp self._strength = strength def full_healing(self) -> None: self._hp = self._MAX_HP self._mp = self._MAX_MP class DarkSlime: _MAX_HP = 150 _MAX_DARK_AURA = 150 def __init__(self, hp: int, dark_aura: int) -> None: self._hp = hp self._dark_aura = dark_aura def full_healing(self) -> None: self._hp = self._MAX_HP self._dark_aura = self._MAX_DARK_AURA class Inn: def stay(self, characters: List[PlayableCharacter]) -> None: for character in characters: character.full_healing() def main() -> None: hero = Hero(50, 50, 50) dark_slime = DarkSlime(120, 120) inn = Inn() inn.stay([hero, dark_slime]) assert hero._hp == 100 assert dark_slime._hp == 150 print("successful!") if __name__ == "__main__": main()
前回から内容を少し変えて、DarkSlimeを追加しました。これはMPの代わりにDarkAuraを回復するようになっていますが、stayとしてはfull_healingのメソッドを持つかの構造を見ている
為、実行も正常に完了しますし、mypyによるエラーも起こりません。
$ python main.py successful! $ mypy main.py --strict Success: no issues found in 1 source file
勿論、Protocolを継承したClassは抽象基底クラス同様にインスタンス化
することもできません。(これがとてもうれしい)
from typing import Protocol class PlayableCharacter(Protocol): def full_healing(self) -> None: pass playable = PlayableCharacter() # TypeError: Protocols cannot be instantiated
pythonは動的型付け言語でダックタイピングを自然と用いる
為、nominal typingよりもStructural Subtypingとの相性のほうが良いと感じました。
ただ、より堅牢なシステム構成が必要な際にはnominal typingのほうが良いかなと思います。
(要は一長一短ですね)
前回記事でEchoのNewHTTPErrorのカスタムレスポンス方法について触れたのですが、errorインターフェースの実装には触れておらず、丁度良いのでここで、errorインターフェースと判定される振る舞いを実装してみます。
前回記事はこちら
package main import ( "net/http" "github.com/labstack/echo/v4" "strings" ) type CustomError []string func (e CustomError) Error() string { return strings.Join(e, ", ") } func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return echo.NewHTTPError(http.StatusUnauthorized, CustomError{"エラーなのだ", "エラーなのだ2"}) }) e.Logger.Fatal(e.Start(":1323")) }
$ curl http://localhost:1323 {"message":"エラーなのだ, エラーなのだ2"}
CustomErrorはerrorインターフェースとしての振る舞い
が定義されている為、DefaultHTTPErrorHandlerのswitch文でerrorインターフェースとして判定され上記のような結果となっています。
ただ、errorインターフェースでErrorメソッドの戻り値がstring
と定義されている為、CustomErrorの戻り値もstring
にしてあげないといけない関係上、完全に前回の結果と同一ではないですが...(ジェネリクス等にしてほしかった感)
もし、配列とかにしたいならjson.Marshaler君のcase判定されれば良いのかなぁ感。
そりでは