Pythonプログラミングイントロダクション(8章) リスコフの置換原則(LSP)をPyhonで考える

2019–11–17

6,7章はさらっと読み進められましたので飛ばして、今回は8章をみていきます。この章ではオブジェクト指向についての説明がされています。 継承、カプセル化、オーバーライド等のオブジェクト指向やクラスの定番の話題が説明がされている中で、リスコフの置換原則について軽く触れられていましたので詳しくみていきたいと思います。

リスコフの置換原則

本書ではリスコフの置換原則についてはさらっと説明されていますが、詳しく見ていきたいと思います。 下記はリスコフの置換原則に関する引用です。

サブクラスを用いてクラス階層を定義する時、
サブクラスはそのスーパークラスの振る舞いを拡張するように作るべきである.

もしスーパークラスのインスタンスを用いたコードが正しく動作するならば、
それはサブクラスのインスタンスに置き換えても正しく動作するように設計すべきである。	  

SuperHogeを継承したSubHogeのインスタンスがFugaServiceで利用されるケースを考えます。 FugaServiceのprocessメソッドではSuperHoge、SubHogeの両方のインスタンスを受け取れますが、どちらが渡ってきても動作します。 動作した結果が意図通りであれば問題ありません。

class FugaService:
    def process(self, hoge):
        hoge.echo()


class SuperHoge:
    def echo(self):
        print('super hoge')

class SubHoge(SuperHoge):
    def echo(self):
        print('sub hoge')


service = FugaService()
super_hoge = SuperHoge()
sub_hoge=SubHoge()

service.process(super_hoge)
service.process(sub_hoge)	  
: super hoge
: sub hoge	  

下記の例は意図通りではない継承のパターンです。 Employeeクラスを継承するEngineerクラスがあり、在職期間を管理しています。 親クラスのEmployeeクラスのperiodメソッドは'年'を引数にとることを前提としていますが、子クラスのEngineerクラスでは'月'を引数にとることを前提としています。 在職期間を元に有給付与とかしているプログラムがあったりるとバグってしまいますね。

class Employee:
    year = 0
    month = 0
    def period(self, year):
        self.year = int(year)
        self.month = int((year - int(year)) * 12)


class Engineer(Employee):
    def period(self, month):
        self.year = int(month / 12)
        self.month = int(month % 12)

def print_exist(employee, year):
    employee.period(year)
    print('在職期間は' , employee.year , '年' , employee.month , 'ヶ月です')

employee = Employee()
engineer = Engineer()

print_exist(employee, 1.5)
print_exist(engineer, 1.5)	  
: 在職期間は 1 年 6 ヶ月です
: 在職期間は 0 年 1 ヶ月です	  

意図通りでないオーバーライドがある場合、インスタンスのタイプによって処理を分けるようなif文を書いたりする必要があります。上記の例だとEmpoyeeだと年を、Engineerだと月を引数に渡すような処理をif文で加えます。

上記の例の解決策もそうなのですが、そもそもメソッドのオーバーライドをやめさせたい場合もあるかと思います。常に親クラスのメソッドが呼ばれるようにしたい場合です。その場合は単に子クラスでのオーバーライドを止めれば良いです。

class FugaService:
    def process(self, hoge):
        hoge.echo()


class SuperHoge:
    def echo(self):
        print('super hoge')

class SubHoge(SuperHoge):
    pass

service = FugaService()
super_hoge = SuperHoge()
sub_hoge=SubHoge()

service.process(super_hoge)
service.process(sub_hoge)	  
: super hoge
: super hoge	  

オーバーライドをやめるのは簡単ですが、SuperHogeクラスを継承したサブクラスにそもそもオーバーライドさせないようにする方が便利です。 Python3.8以降であれば@finalを利用するのが一つの手です。

from typing import final

class FugaService:
    def process(self, hoge):
        hoge.echo()


class SuperHoge:
    @final
    def echo(self) -> None:
        print('super hoge')

class SubHoge(SuperHoge):

    def echo(self) -> None:
        print('sub hoge')

service = FugaService()
super_hoge = SuperHoge()
sub_hoge=SubHoge()

service.process(super_hoge)
service.process(sub_hoge)
	  

上記のファイルをhoge.pyとして保存して、mypyコマンドで型チェックをすると、下記のようなエラーになります。

❯ mypy hoge.py
hoge.py:15: error: Cannot override final attribute "echo" (previously declared in base class "SuperHoge")
Found 1 error in 1 file (checked 1 source file)	  

型チェッカーはPythonランタイムとは別のものなので、開発環境に組み込んだりCIの一環として実装する必要があります。

まとめ

8章からリスコフの置換原則について取り上げました。何気に使っているオーバーライドですがリスコフの置換原則を守ることで、無駄なif文がない見通しの良いプログラムになります。