#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
типа он использовал aTypeVar
с привязкой к наименее производному типу,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, если вы написали его наоборот.