Аннотирование возвращаемых типов для методов, возвращающих self в миксинах - PullRequest
0 голосов
/ 26 мая 2020

Я использую шаблон построителя, в котором большинство методов в (большом) классе возвращают свою идентичность (self) и, таким образом, аннотируются, чтобы вернуть тип класса, членом которого они являются:

class TextBuilder:
    parts: List[str]           # omitted
    render: Callable[[], str]  # for brevity

    def text(self, val: str) -> "TextBuilder":
        self.parts.append(val)
        return self

    def bold(self, val: str) -> "TextBuilder":
        self.parts.append(f"<b>{val}</b>")
        return self

    ...

Пример использования:

joined_text = TextBuilder().text("a ").bold("bold").text(" text").render()  
# a <b>bold</b> text

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

class BaseBuilder:
    parts: List[str]           # omitted
    render: Callable[[], str]  # for brevity


class TextBuilder(BaseBuilder):
    def text(self, val: str):
        self.parts.append(val)
        return self
    ...


class HtmlBuilder(BaseBuilder):
    def bold(self, val: str):
        self.parts.append(f"<b>{val}</b>")
        return self
    ...


class FinalBuilder(TextBuilder, HtmlBuilder):
    pass

Однако я не вижу способ правильно аннотировать возвращаемые типы классов миксинов таким образом, чтобы результирующий класс FinalBuilder всегда заставлял mypy полагать, что он возвращает FinalBuilder, а не один из классов миксинов. Все это, конечно, предполагает, что я хочу на самом деле аннотировать self и возвращаемые типы, потому что они не могут быть выведены из того, что происходит внутри этих методов. c и явно помечая их как возвращающие тип T, связанный с BaseBuilder, но это не удовлетворило mypy. Любые идеи? На данный момент я просто собираюсь пропустить все эти махинации и опустить типы возвращаемых значений везде, так как они должны быть правильно выведены при использовании FinalBuilder, но мне все еще любопытно, есть ли общий способ подойти к этому.

1 Ответ

1 голос
/ 26 мая 2020

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

from typing import List, Callable, TypeVar

T = TypeVar('T', bound=BaseBuilder)

class BaseBuilder:
    parts: List[str]           # omitted
    render: Callable[[], str]  # for brevity


class TextBuilder(BaseBuilder):
    def text(self: T, val: str) -> T:
        self.parts.append(val)
        return self
    ...


class HtmlBuilder(BaseBuilder):
    def bold(self: T, val: str) -> T:
        self.parts.append(f"<b>{val}</b>")
        return self
    ...


class FinalBuilder(TextBuilder, HtmlBuilder):
    pass


# Type checks
f = FinalBuilder().text("foo").bold("bar")

# Mypy states this is type 'FinalBuilder'
reveal_type(f)

Несколько примечаний:

  1. Если мы не аннотируем self, mypy обычно будет предполагать, что это тип того класса, в котором мы в настоящее время содержатся. Однако на самом деле можно дать ему подсказку настраиваемого типа, если вы хотите, если это подсказка типа совместима с классом. (Например, было бы незаконно добавлять def foo(self: int) -> None в HtmlBuilder, поскольку int не является супертипом HtmlBuilder.)

    Мы воспользуемся этим, сделав self generi c так мы можем указать более конкретный c возвращаемый тип.

    Подробнее см. в документации mypy: https://mypy.readthedocs.io/en/stable/generics.html#generic -methods-and-generi c -self

  2. Я ограничил TypeVar значением BaseBuilder, чтобы обе функции могли видеть поля parts и render. Если вы хотите, чтобы ваши функции text(...) и bold(...) также видели поля, определенные в TextBuilder и HtmlBuilder соответственно, вам необходимо создать две TypeVars, привязанные к этим более детальным c дочерним классам.

...