Как заставить python распознавать подклассы как допустимые типы, когда он ожидает их родительский класс?

Вот минимальный пример того, что мне нужно сделать:

from typing import Callable, Any


class Data:
    pass


class SpecificData(Data):
    pass


class Event:
    pass


class SpecificEvent(Event):
    pass


def detect_specific_event(data: SpecificData, other_info: str) -> SpecificEvent:
    return SpecificEvent()


def run_detection(callback: Callable[[Data, Any], Event]) -> None:
    return


run_detection(detect_specific_event)

Теперь я получаю предупреждение:

Expected type '(Data, Any) -> Event', got '(data: SpecificData, other_info: str) -> SpecificEvent' instead 

Мне кажется, что это предупреждение не имеет смысла, поскольку SpecificData и SpecificEvent являются подтипами Data и Event соответственно, так что все должно быть в порядке. Есть ли способ сделать эту работу, как я ожидаю? Моя идея состоит в том, чтобы иметь возможность иметь что-то вроде:

class OtherSpecificData(Data):
    pass


class OtherSpecificEvent(Event):
    pass


def detect_other_event(data: OtherSpecificData, other_info: str) -> OtherSpecificEvent:
    return OtherSpecificEvent()

run_detection(detect_other_event)

поэтому функция run_detection является максимально общей. Прямо сейчас это дает то же предупреждение, что и выше.


person Petar Chernev    schedule 28.02.2020    source источник
comment
Проблема здесь в том, что ваш тип аргумента в run_detection подразумевает, что переданный вызываемый объект должен иметь возможность работать с Data и всеми его подклассами, но затем вы передаете ему вызываемый объект, который говорит, что он не может работать с Data, он может работать только с SpecificData. Например, предположим, что detect_specific_event использует атрибут в SpecificData, которого нет в родительском классе. Но run_detection сообщается, что ожидаемый обратный вызов этого не сделает; сказано, что он будет работать на Data и всех его подклассах. Тогда почему переданная функция требует SpecificData?   -  person alkasm    schedule 01.03.2020
comment
Похоже, вы ожидаете, что родительский класс будет действовать как объединение своих подклассов, но это не так, потому что подклассы могут иметь функциональность, которой нет у родителя. Если run_detection может вызывать обратный вызов независимо от типа данных, то вы просите функцию требовать, чтобы вызываемый объект был более конкретным (работает только с базовым классом), чем вы хотите (работает с любым подклассом). Что касается run_detection, отношения между классами не имеют значения. Простое решение — просто использовать Union ваших подтипов. В противном случае ищите композицию вместо наследования.   -  person alkasm    schedule 01.03.2020
comment
@Alkasm Я в основном согласен, но ... две функции здесь могли бы работать, имея в виду LSP, если бы дополнительные данные, которые здесь не отображаются, были конкретными данными. Но код OP показывает только подключение run_detection, о чем они спрашивали здесь, а не о том, как он получает данные. Если в следующий раз запустить только генератор SpecificData, то он заработает. Но не в том случае, если он выдает Data событий.   -  person JL Peyret    schedule 01.03.2020
comment
@JLPeyret действительно мои комментарии делают предположение о том, что намеревается сделать ОП, что может быть правдой, а может и нет. Но поскольку в конце OP говорится, что run_detection является максимально общим, главное, что нужно здесь понять, это то, что run_detection ожидание вызываемого объекта в базовом классе более ограничительно, а не более общее.   -  person alkasm    schedule 02.03.2020


Ответы (2)


Подтип параметра противоположен обратному подтипу.

  • Возвращаемое значение назначается от вызываемого абонента вызывающему.
  • Значение параметра присваивается от вызывающего к вызываемому.

И присваиваемое значение должно быть более конкретным, чем ожидаемый тип переменной. Например:

data: Data = SpecificData()  # okay
data: SpecificData = Data()  # not okay

Итак, вы должны сделать:

from typing import Callable, Any


class Data:
    pass


class SpecificData(Data):
    pass


class Event:
    pass


class SpecificEvent(Event):
    pass


def detect_specific_event(data: Data, other_info: str) -> SpecificEvent:
    return SpecificEvent()


def run_detection(callback: Callable[[SpecificData, Any], Event]) -> None:
    return


run_detection(detect_specific_event)
person Boseong Choi    schedule 28.02.2020
comment
Спасибо, это проясняет, почему я получаю предупреждение. Означает ли это, что нет способа определить run_detection так, чтобы он мог принимать как detect_specific_event, так и detect_other_event из моего примера? - person Petar Chernev; 28.02.2020
comment
Я думаю так. Обеим функциям нужны разные конкретные типы, поэтому их нельзя обобщать. Я предлагаю изменить тип параметра detect_specific_event на Data, Any, если вам нужно обобщение. - person Boseong Choi; 28.02.2020
comment
хм, извините, не зная так много о наборе текста, это не имеет концептуального смысла. судя по его названию, я бы предположил, что detect_specific_event хочет SpecificData и у него могут возникнуть проблемы с более общими версиями. тем не менее, чтобы сделать ввод счастливым, ваши сигнатуры функций передают человеческому читателю прямо противоположное значение. - person JL Peyret; 28.02.2020

Мне потребовалось некоторое время, чтобы вспомнить, какую часть ввода использовать, но ИМХО вы хотите использовать приведение

В отличие от использования в других языках, cast(x,y) ничего не делает, но сообщает вводу рассматривает y как тип x. время выполнения, это не работает, просто возвращает y.

Как и в скомпилированных языках, если бы я прочитал это, я бы обратил особое внимание на код: действительно ли он будет работать во время выполнения? действительно ли типы данных будут правильными?:

  • Двойное закрытие с примечанием LSP уместно, если вы не можете гарантировать, что то, что позже генерирует данные, будет раздавать только SpecificDatas. Если вы можете, то кастинг будет в порядке. В вашем минимальном примере этот бит отсутствует, но если бы вы показали, какие фактические данные проходят через print(data), мы бы знали, применяется ли LSP.
from typing import Callable, Any, cast


class Data:
    pass


class SpecificData(Data):
    pass


class Event:
    pass


class SpecificEvent(Event):
    pass


def detect_specific_event(data: SpecificData, other_info: str) -> SpecificEvent:
    return SpecificEvent()


def run_detection(callback: Callable[[Data, Any], Event]) -> None:
    return


run_detection(cast((Callable[[Data, Any], Event]),detect_specific_event))

Здесь вы в основном сказали, набрав «примите мое слово», что detect_specific_event - это Callable[[Data, Any], Event]).

результаты прогонов и проверки типов:

$ mypy test2.py
Success: no issues found in 1 source file
$ python test2.py
(venv)$  ???? well your code says nothing.

изменить приведение к фактическому сигналу:

run_detection(cast((Callable[[SpecificData, Any], SpecificEvent]),detect_specific_event))

(venv) [email protected]$ mypy test2.py
Argument 1 to "run_detection" has incompatible type "Callable[[SpecificData, Any], SpecificEvent]"; expected "Callable[[Data, Any], Event]"
Found 1 error in 1 file (checked 1 source file)
$ python test2.py 
$???? well your code says nothing.
person JL Peyret    schedule 01.03.2020