Сужение подсказок типа у детей

Мне было интересно, является ли введение более жестких ограничений на подсказки типов в дочерних классах плохой практикой или нет. Рассмотрим следующий пример

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 ровно три. Является ли такое усиление ограничений плохой практикой? Спасибо!

С иерархической точки зрения экземпляр Baz — это экземпляр Bar, являющийся экземпляром Foo, и его можно заменить везде, где ожидается экземпляр Bar или Foo. Но он отклонит переданные ему аргументы, которые Foo или Bar примут. Обычно это не очень разумно.

deceze 01.07.2024 14:19

Обратите внимание, что список из двух строк — это не то же самое, что кортеж из двух строк, поэтому возможные аргументы Foo.print_stuff() не являются надмножеством аргументов Bar.print_stuff().

macjan 01.07.2024 14:24

Хорошо, но как лучше всего обеспечить это? Я хотел бы заставить дочерние классы реализовать print_stuff, но аргументы (или выходные данные) могут отличаться. Могу ли я воздержаться от набора текста здесь? Должен ли я полностью удалить абстрактный метод?

DeerFreak 01.07.2024 14:27

Вы можете сделать Foo универсальным в этом типе. Создание подклассов, параметризованных разными типами, не является нарушением LSP и ясно дает понять ваше намерение: вам нужен подкласс, который может что-то делать с элементами типа T, который является подтипом tuple[str, ...].

STerliakov 01.07.2024 14:35
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
4
56
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете использовать многоточие, чтобы указать, что количество аргументов в кортеже является переменным. Вам следует избегать использования 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.")

Спасибо :) Но считается ли это здесь плохой практикой? Потому что я больше не смогу менять детей Фу.

DeerFreak 01.07.2024 14:28

правильный. эта разная длина возврата нарушает замену Лискова.

James 01.07.2024 14:30

Запуск 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.")

То есть, если я хочу придерживаться принципа Лискова, мне следует полностью удалить абстрактный метод? Спасибо!

DeerFreak 01.07.2024 14:29

Я не думаю, что это связано с абстрактным методом, это означает лишь то, что метод базового класса не имеет реализации и его необходимо переопределить в каждом дочернем классе.

mkrieger1 01.07.2024 14:44

Единственная причина переопределить абстрактный метод — это наследовать интерфейс, но вы (ОП) здесь этого не делаете. Foo.print_stuff обещает один тип, но ваши реализации требуют другого. Foo.print_stuff может понадобиться, а может и не понадобиться, но Bar и Baz могут также определять отдельные, несвязанные функции с разными именами.

chepner 01.07.2024 15:44

Другие вопросы по теме