はじめに
fortee連携 やりました!! nikkieです。
mypyのドキュメントの「Incompatible overrides」を見ていきます。
これはSOLID原則の1つと重なります
目次
- はじめに
- 目次
- リスコフの置換原則
- mypyドキュメントの「Incompatible overrides」
- いずれも基底クラスの使い方を壊してはならない
- 終わりに
- 動作環境
- P.S. 直近見たSOLID原則
- P.S. Pydanticで違反せざるを得なかった例
リスコフの置換原則
SOLID原則のLです。
正しい継承について伝えています。
あるクラスを継承した派生クラスについて、元のクラスと派生クラスはまったく同じ使い方を保証しなければなりません。
すでに定着している仕様を勝手に変えてしまうのを避ける (ちょうぜつ本 Kindle版 p.168)
全く同じ使い方には、メソッドの引数の型(事前条件)と返り値の型(事後条件)も関わります。
この点をmypyに怒られながら、再確認します。
mypyドキュメントの「Incompatible overrides」
https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
リスコフの置換原則に抵触する場合が2つ述べられています。
override a method with a more specific argument type
override a method with a more general return type.
ポイントは「派生クラスを元のクラスと同じように使えるか」と理解しました。
前提:型の関係
リスト、シーケンス、イテラブル、3つの型が登場します。
- 最も狭いのはリスト
- 最も広いのはイテラブル
- シーケンスはイテラブル(for文で反復できる)が、シーケンスでないイテラブルもある(例:ファイルオブジェクト)
事前条件:引数の型は広げてよいが、狭めてはならぬ
基底クラスのメソッドの引数の型はSequence[int]
です(例えばlist[int]
やtuple[int]
)。
- この引数の型を
Iterable[int]
へ広げる派生クラスGeneralizedArgument
- mypy「いいね👍」 問題なし
- 派生クラスに
Sequence[int]
は渡し続けられる
list[int]
と狭める派生クラスNarrowerArgument
- mypyが指摘
- 派生クラスに
tuple[int]
が渡せなくなってしまった(なので、Sequence[int]
を渡し続けられない)
% python -m mypy lsp_argument.py lsp_argument.py:16: error: Argument 1 of "test" is incompatible with supertype "A"; supertype defines the argument type as "Sequence[int]" [override] lsp_argument.py:16: note: This violates the Liskov substitution principle lsp_argument.py:16: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
事後条件:返り値の型は狭めてよいが、広げてはならぬ
基底クラスのメソッドの返り値の型はSequence[str]
です。
- この返り値の型を
list[str]
へ狭める派生クラスNarrowerReturn
- mypy「いいね👍」 問題なし
- 派生クラスの返り値を
Sequence[str]
と見なせる
Iterable[str]
と広げる派生クラスGeneralizedReturn
- mypyが指摘
- 派生クラスでは
Iterable[str]
と広がった - シーケンスの動きを満たさないがIterableなオブジェクトが返りうる
- このとき、
Sequence[str]
が返るとしてシーケンスのインデックスアクセスをしている既存コードは壊れてしまう
- このとき、
% python -m mypy lsp_return.py lsp_return.py:16: error: Return type "Iterable[str]" of "test" incompatible with return type "Sequence[str]" in supertype "A" [override]
いずれも基底クラスの使い方を壊してはならない
リスコフの置換原則に則るには、
- メソッドの引数の型は、同じか広げる
- メソッドの返り値の型は、同じか狭める
違反してしまう場合
- 引数の型を狭めてしまうと、基底クラスと同じ実引数を派生クラスに渡せないことになってしまう
- 返り値の型を広げてしまうと、返り値の処理のコードに想定していない値が渡ってしまう(動かなくなりうる)
終わりに
mypyのドキュメントに沿って手を動かしながら、リスコフの置換原則の理解を深めました。
基底クラスを使っているコードを派生クラスに置き換えたとき、コードが動き続けられるならばリスコフの置換原則を満たしています。
以下を派生クラスでやってしまうと、既存クラスを使っているコードが動かなくなりうるので、置換原則違反です。
- 引数の型を狭めてしまう
- 返り値の型を広げてしまう
動作環境
% python -V Python 3.10.9 % python -m mypy --version mypy 1.8.0 (compiled: yes)
P.S. 直近見たSOLID原則
PHPカンファレンス北海道2024より🦀
P.S. Pydanticで違反せざるを得なかった例
PydanticのBaseModelを継承したクラスで__iter__
を実装しました。
ここはリスコフの置換原則違反の指摘を受け入れる(ignoreする)選択をしました。
BaseModelの__iter__
の返り値の型はtyping.Generator[typing.Tuple[str, Any], None, None]
です
- https://github.com/pydantic/pydantic/blob/v2.5.3/pydantic/main.py#L908-L913
- https://github.com/pydantic/pydantic/blob/v2.5.3/pydantic/main.py#L46
BaseModelを継承して定義したクラスで、タプルを要素とするジェネレータ以外を返したい場合、# type: ignore[override]
せざるを得ないと考えています。