Подсказка для метода абстрактного класса, который возвращает экземпляр класса

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

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

from abc import ABC, abstractmethod
from typing import TypeVar


TMetricBase = TypeVar("TMetricBase", bound="MetricBase")


class MetricBase(ABC):
    @abstractmethod
    def add(self, element: str) -> None:
        pass  # pragma: no cover

    @classmethod
    @abstractmethod
    def decode(cls, json_str: str) -> TMetricBase:
        pass  # pragma: no cover


Дочерний класс выглядит следующим образом

import json
from typing import Any, Callable, List, Mapping, Optional
from something import MetricBase, TMetricBase


class DiscreteHistogramMetric(MetricBase):
    def __init__(self, histogram: Optional[Mapping[str, int]]) -> None:
        super().__init__()
        self._histogram = dict(histogram) if histogram else {}

    def add(self, element: str) -> None:
        self._histogram[element] = self._histogram.get(element, 0) + 1

    @classmethod
    def decode(cls, json_str: str) -> "DiscreteHistogramMetric":
        json_obj = json.loads(json_str)
        histogram_map = json_obj["DiscreteHistogramMetric"]
        return cls(histogram=histogram_map)

Я получаю следующую ошибку:

error: Return type of "decode" incompatible with supertype "MetricBase"

При изменении типа возврата decode на TMetricBase я получаю следующую ошибку:

error: Incompatible return value type (got "DiscreteHistogramMetric", expected "TMetricBase")

person IceCube    schedule 16.06.2019    source источник
comment
Возможно, дубликат Как аннотировать, что метод класса возвращает экземпляр этого класса?   -  person Aran-Fey    schedule 16.06.2019


Ответы (1)


Ошибка связана с тем, что у вас есть только одна TypeVar в возвращаемом типе decode. Неясно, что именно это будет означать - вы более или менее пытаетесь объявить, что каждый отдельный подкласс MetricBase должен поддерживать возврат любого другого произвольного подкласса MetricBase, который он каким-то волшебным образом выведет на основе того, как эта функция выполняется. называется.

На самом деле это не то, что можно сделать в Python.

Вместо этого вам нужно сделать одно из следующего:

  1. Откажитесь и не используйте TypeVars
  2. Сделайте MetricBase общим классом, а ваши подклассы наследуют параметризованную версию MetricBase.
  3. Каким-то образом используйте TMetricBase в параметрах decode. (Таким образом, мы можем определить, каким должен быть возвращаемый тип).

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

Второе решение выглядит примерно так:

from abc import ABC, abstractmethod
from typing import TypeVar, Generic

TMetricBase = TypeVar("TMetricBase", bound="MetricBase")

class MetricBase(ABC, Generic[TMetricBase]):
    @classmethod
    @abstractmethod
    def decode(cls, json_str: str) -> TMetricBase:
        pass

class DiscreteHistogramMetric(MetricBase['DiscreteHistogramMetric']):
    @classmethod
    def decode(cls, json_str: str) -> "DiscreteHistogramMetric":
        pass

Имея DiscreteHistogramMetric подкласс MetricBase[DiscreteHistogramMetric], а не только MetricBase напрямую, мы можем фактически ограничить переменную типа чем-то значимым.

Это решение по-прежнему немного неуклюже — создание подкласса MetricBase требует, чтобы мы начали использовать дженерики везде, где мы используем MetricBase, что довольно раздражает.

Третье решение на первый взгляд звучит еще более неуклюже: мы собираемся добавить какой-то дополнительный фиктивный третий параметр или какую-то ерунду? Но оказывается, есть хороший трюк, который мы можем использовать — мы можем использовать общие селфи, чтобы аннотировать переменную cls!

Обычно тип этой переменной выводится, и его не нужно аннотировать, но в этом случае полезно сделать это: мы можем использовать информацию о том, что такое cls, чтобы получить более точный возвращаемый тип.

Вот как это выглядит:

from abc import ABC, abstractmethod
from typing import TypeVar, Type

TMetricBase = TypeVar("TMetricBase", bound="MetricBase")

class MetricBase(ABC):
    @classmethod
    @abstractmethod
    def decode(cls: Type[TMetricBase], json_str: str) -> TMetricBase:
        pass

class DiscreteHistogramMetric(MetricBase):
    def __init__(self, something: str) -> None:
        pass

    @classmethod
    def decode(cls: Type[TMetricBase], json_str: str) -> TMetricBase:
        # Note that we need to use create the class by using `cls` instead of
        # using `DiscreteHistogramMetric` directly.
        return cls("blah")

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

Однако это делает свое дело: выполнение DiscreteHistogramMetric.decode("blah") вернет TMetricBase, как и ожидалось.

И в отличие от первого подхода, беспорядок, по крайней мере, довольно хорошо ограничен методом decode и не требует, чтобы вы начали использовать дженерики везде, где вы также используете классы MetricBase.

person Michael0x2a    schedule 16.06.2019