概要
背景として、後述のDjangoのModel定義でリスコフの置換原則満たさない書き方をみんなしているのではないかという疑問が出てきたので、軽くまとめました。
関数の場合
以下の継承は、reverse の型が変わっているので、リスコフの置換原則を満たしません。
class Parent:
def reverse(self, x: int) -> int:
return -x
class Child(Parent):
def reverse(self, x: str) -> str:
return x[::-1]
同じく、一見大丈夫そうに見えますが、@proparty
を用いた場合もリスコフの置換原則を満たしません。
Parentではnameメソッドですが、Childではnameフィールドになっています。
class Parent:
@property
def name(self) -> str:
return "parent"
class Child(Parent):
name = "child"
以下のように修正しなければなりません。
class Parent:
@property
def name(self) -> str:
return "parent"
class Child(Parent):
@property
def name(self) -> str:
return "child"
インナークラスの場合
以下はインナークラスを使用したリスコフの置換原則を満たさない例です。
class Car:
class Engine:
gas = True
class EVCar(Car):
class Engine:
battery = True
car = Car()
engine = car.Engine()
print(engine.gas) # True
ev = EVCar()
engine = ev.Engine()
print(engine.gas) # AttributeError
print(engine.battery) # True
上記は継承しているのにも関わらず、Engineをオーバーライドしてしまっています。EVCarのgas属性がないので、EVCar is Carの関係が成り立ちません。これはリスコフの置換原則を満たしません。
以下のようにすると、EVCarでCarのEngineも継承できております。
class Car:
class Engine:
gas = True
class EVCar(Car):
class Engine(Car.Engine):
battery = True
car = Car()
engine = car.Engine()
print(engine.gas) # True
ev = EVCar()
engine = ev.Engine()
print(engine.gas) # True
print(engine.battery) # True
しかし、上記だと、EVCar の engineの gas がTrueになってしまいます。そこで、EVCar のEngineで明示的に gas を False にします。
class Car:
class Engine:
gas = True
class EVCar(Car):
class Engine(Car.Engine):
battery = True
gas = False
car = Car()
engine = car.Engine()
print(engine.gas) # True
ev = EVCar()
engine = ev.Engine()
print(engine.gas) # False
print(engine.battery) # True
DjangoのAbstractBaseModel
Djangoでは抽象モデルとして、共通フィールドを AbstractBaseClass
で定義することがあります。
例えば以下のようにcreate_atを共通化します。AbstractModel
のメタデータにabstract = True
があるので、AbstractModelはデータベースにマッピングされません。
from django.db import models
class AbstractModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class Foo(ModelTemplate):
class Meta:
ordering = ["id"]
よく見かけるこの書き方、継承しているのにも関わらず、オーバライドしています。
そこで以下のように修正します。
from django.db import models
class AbstractModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class Foo(ModelTemplate):
class Meta(ModelTemplate.Meta):
ordering = ["id"]
実行してみます。
from public_service.models.a import ModelTemplate,Foo
a = Foo()
a.Meta().abstract # False
abstract=True
が継承されているので、a.Meta().abstract
は True になると思ったのですが、 False になりました。models.Model
の実装を見ると、_meta
でabstractがなければ自動的に falseが設定されるようになっています。(実装の説明は割愛)
最後に
Pylanceでは reportAssignmentType
として、継承時にオーバーライドしてしまっているものを検知することができます。