Переопределите тип аргумента в подтипе с помощью подтипа

#python #typing #mypy

Вопрос:

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

 Logic   Logic => Logic
Logic   Bit   => Logic
Bit   Logic   => Logic
Bit   Bit     => Bit
 

Пример:

 class Logic:
    """
    4-value logic type: 0, 1, X, Z
    """

    def __and__(self, other: 'Logic') -> 'Logic':
        if not isinstance(other, Logic):
            return NotImplemented
        # ...

    def __rand__(self, other: 'Logic') -> 'Logic':
        return self amp; other


class Bit(Logic):
    """
    2-value bit type: 0, 1

    As a subtype of Logic, Logic(0) == Bit(0) and hash(Logic(0)) == hash(Bit(0))
    """

    def __and__(self, other: 'Bit') -> 'Bit':
        if not isinstance(other, Bit):
            return NotImplemented
        # ...

    def __rand__(self, other: 'Bit') -> 'Bit':
        return self amp; other

 

Хотя это работает во время выполнения, mypy жалуется:

 example.pyi:19: error: Argument 1 of "__and__" is incompatible with supertype "Logic"; supertype defines the argument type as "Logic"
example.pyi:19: note: This violates the Liskov substitution principle
example.pyi:19: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
 

Как мне выразить эти отношения? Я чувствую, что проблема может быть связана с тем фактом, что Python не поддерживает множественную отправку, и это закодировано в системе типов mypy таким образом, что я не могу это выразить. Возможно, mypy здесь слишком придирчив , это должно быть нормально, потому Bit что аргумент, упомянутый в Bit.__and__ , является подтипом Logic , поэтому «несовместимый» неверен.

Ответ №1:

Вы создаете подкласс и перезаписываете функцию из класса, от которого вы наследуете. Это нормально, если вы не измените подпись (вот почему mypy жалуется). Есть несколько возможных решений, но вы могли бы попробовать это:

 from __future__ import annotations


class Base:
    ...


class Logic:
    """
    4-value logic type: 0, 1, X, Z
    """

    def __and__(self, other: Base) -> Logic:
        if not isinstance(other, Logic):
            raise NotImplementedError()
        return self

    def __rand__(self, other: Base) -> Logic:
        return self amp; other


class Bit(Logic):
    """
    2-value bit type: 0, 1

    As a subtype of Logic, Logic(0) == Bit(0) and hash(Logic(0)) == hash(Bit(0))
    """

    def __and__(self, other: Base) -> Bit:
        if not isinstance(other, Bit):
            raise NotImplementedError()
        return self

    def __rand__(self, other: Base) -> Bit:
        return self amp; other
 

Комментарии:

1. После поднятия этой проблемы в репозитории GH я получил предложение использовать что-то подобное этому, но вместо создания Base типа он использовал a TypeVar с привязкой к наименее производному типу, Logic .

Ответ №2:

Это идеальный вариант использования для typing.overload . Вы можете указать несколько подписей для одной и той же функции и пометить их как @overload , и mypy разрешит вызовы соответствующей перегрузки.

Для вашего примера вы можете перегрузить методы __add__ и __radd__ в Bit классе (см. Выходные данные mypy в mypy-play).:

 from typing import overload

class Logic:
    def __and__(self, other: 'Logic') -> 'Logic':
        pass

class Bit(Logic):
    @overload
    def __and__(self, other: 'Bit') -> 'Bit': ...
    
    @overload
    def __and__(self, other: 'Logic') -> 'Logic': ...
    
    def __and__(self, other):
        pass

reveal_type(Logic() amp; Logic())  # Logic
reveal_type(Logic() amp; Bit())    # Logic
reveal_type(Bit() amp; Logic())    # Logic
reveal_type(Bit() amp; Bit())      # Bit
 

Обратите внимание, что перегрузки сопоставляются по порядку, поэтому Bit перегрузка должна отображаться перед Logic перегрузкой. Вы получите предупреждение от mypy, если вы написали его наоборот.