Мне было интересно, является ли введение более жестких ограничений на подсказки типов в дочерних классах плохой практикой или нет. Рассмотрим следующий пример
from typing import List, Tuple
from abc import abstractmethod
class Foo:
@abstractmethod
def print_stuff(self, items: List[str]):
"""print some list."""
class Bar(Foo):
def print_stuff(self, items: Tuple[str, str]):
print(f"Items: {items}, always 2.")
class Baz(Foo):
def print_stuff(self, items: Tuple[str, str, str]):
print(f"Items: {items}, always 3.")
Здесь базовый класс ожидает некоторый список строк. В Bar
метод допускает только две строки, а в Baz
ровно три. Является ли такое усиление ограничений плохой практикой?
Спасибо!
Обратите внимание, что список из двух строк — это не то же самое, что кортеж из двух строк, поэтому возможные аргументы Foo.print_stuff() не являются надмножеством аргументов Bar.print_stuff().
Хорошо, но как лучше всего обеспечить это? Я хотел бы заставить дочерние классы реализовать print_stuff, но аргументы (или выходные данные) могут отличаться. Могу ли я воздержаться от набора текста здесь? Должен ли я полностью удалить абстрактный метод?
Вы можете сделать Foo
универсальным в этом типе. Создание подклассов, параметризованных разными типами, не является нарушением LSP и ясно дает понять ваше намерение: вам нужен подкласс, который может что-то делать с элементами типа T, который является подтипом tuple[str, ...]
.
Вы можете использовать многоточие, чтобы указать, что количество аргументов в кортеже является переменным. Вам следует избегать использования List
в родительском классе и Tuple
в дочернем классе, поскольку это принципиально разные типы.
from typing import List, Tuple
from abc import abstractmethod
class Foo:
@abstractmethod
def print_stuff(self, items: Tuple[str, ...]):
"""print some list."""
class Bar(Foo):
def print_stuff(self, items: Tuple[str, str]):
print(f"Items: {items}, always 2.")
class Baz(Foo):
def print_stuff(self, items: Tuple[str, str, str]):
print(f"Items: {items}, always 3.")
Это по-прежнему не очень хорошая практика, поскольку Bar
и Baz
возвращают кортежи разной длины, что нарушает замену Лискова; mypy, вероятно, выдаст ошибку.
Реальный вопрос: должен ли это быть кортеж или допустима любая последовательность? Тогда вам следует использовать:
from typing import Sequence
from abc import abstractmethod
class Foo:
@abstractmethod
def print_stuff(self, items: Sequence[str]):
"""print some list."""
class Bar(Foo):
def print_stuff(self, items: Sequence[str]):
print(f"Items: {items}, always 2.")
class Baz(Foo):
def print_stuff(self, items: Sequence[str]):
print(f"Items: {items}, always 3.")
Спасибо :) Но считается ли это здесь плохой практикой? Потому что я больше не смогу менять детей Фу.
правильный. эта разная длина возврата нарушает замену Лискова.
Запуск mypy (игровая площадка) приводит к следующим ошибкам:
error: Argument 1 of "print_stuff" is incompatible with supertype "Foo"; supertype defines the argument type as "list[str]" [override]
note: This violates the Liskov substitution principle
note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
error: Argument 1 of "print_stuff" is incompatible with supertype "Foo"; supertype defines the argument type as "list[str]" [override]
note: This violates the Liskov substitution principle
note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
Поэтому я бы посчитал это плохой практикой.
При наследовании классов типы параметров метода являются контравариантными, то есть при переопределении метода базового класса, если вы хотите следовать принципу подстановки Лискова, вы можете расширить тип параметра, но не сузить его.
Например, это было бы возможно:
from typing import Tuple
from abc import abstractmethod
class Foo:
@abstractmethod
def print_stuff(self, items: Tuple[str, str]):
"""print some pair (always 2 strings)."""
class Bar(Foo):
def print_stuff(self, items: Tuple[str, int | str]):
print(f"Items: {items}, always 2, one could be an int.")
class Baz(Foo):
def print_stuff(self, items: Tuple[str, str] | None):
print(f"Items: {items}, always 2, or None.")
То есть, если я хочу придерживаться принципа Лискова, мне следует полностью удалить абстрактный метод? Спасибо!
Я не думаю, что это связано с абстрактным методом, это означает лишь то, что метод базового класса не имеет реализации и его необходимо переопределить в каждом дочернем классе.
Единственная причина переопределить абстрактный метод — это наследовать интерфейс, но вы (ОП) здесь этого не делаете. Foo.print_stuff
обещает один тип, но ваши реализации требуют другого. Foo.print_stuff
может понадобиться, а может и не понадобиться, но Bar
и Baz
могут также определять отдельные, несвязанные функции с разными именами.
С иерархической точки зрения экземпляр
Baz
— это экземплярBar
, являющийся экземпляромFoo
, и его можно заменить везде, где ожидается экземплярBar
илиFoo
. Но он отклонит переданные ему аргументы, которыеFoo
илиBar
примут. Обычно это не очень разумно.