Как реализовать интерфейс, совместимый с проверками статического типа?

У меня есть два базовых класса, Foo и Bar, и класс Worker, который ожидает объекты, которые ведут себя как Foo. Затем я добавляю еще один класс, который реализует все соответствующие атрибуты и методы из Foo, но мне не удалось передать это успешно для проверки статического типа через mypy. Вот небольшой пример:

class MyMeta(type):
    pass

class Bar(metaclass=MyMeta):
    def bar(self):
        pass

class Foo:
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass

class Worker:
    def __init__(self, obj: Foo):
        self.x = obj.x

Здесь Worker фактически принимает любой Foo-подобный объект, то есть объекты, имеющие атрибут x и метод foo. Так что если obj ходит как Foo и крякает как Foo, то Worker будет счастлив. Теперь весь проект использует подсказки типов, поэтому на данный момент я указываю obj: Foo. Все идет нормально.

Теперь есть еще один класс FooBar, который является подклассом Bar и ведет себя как Foo, но он не может создавать подкласс Foo, потому что он предоставляет свои атрибуты через свойства (и поэтому параметры __init__ не имеют смысла):

class FooBar(Bar):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    def foo(self):
        pass

На этом этапе выполнение Worker(FooBar()), очевидно, приводит к ошибке средства проверки типов:

error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Foo"

Использование абстрактного базового класса

Чтобы передать интерфейс Foo-ish контролеру типов, я подумал о создании абстрактного базового класса для типов Foo-ish:

import abc

class Fooish(abc.ABC):
    x : int

    @abc.abstractmethod
    def foo(self) -> int:
        raise NotImplementedError

Однако я не могу сделать FooBar наследовать от Fooish, потому что Bar имеет свой собственный метакласс, и это может вызвать конфликт метакласса. Итак, я подумал об использовании Fooish.register на Foo и FooBar, но mypy не согласен:

@Fooish.register
class Foo:
    ...

@Fooish.register
class FooBar(Bar):
    ...

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x

При этом возникают следующие ошибки:

error: Argument 1 to "Worker" has incompatible type "Foo"; expected "Fooish"
error: Argument 1 to "Worker" has incompatible type "FooBar"; expected "Fooish"

Использование "обычного" класса в качестве интерфейса

Следующий вариант, который я рассмотрел, - это создание интерфейса без наследования от abc.ABC в форме "нормального" класса, а затем от него наследовать как Foo, так и FooBar:

class Fooish:
    x : int

    def foo(self) -> int:
        raise NotImplementedError

class Foo(Fooish):
    ...

class FooBar(Bar, Fooish):
    ...

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x

Теперь mypy не жалуется на тип аргумента Worker.__init__, но жалуется на несовместимость подписи FooBar.x (это property) с Fooish.x:

error: Signature of "x" incompatible with supertype "Fooish"

Также базовый класс Fooish (абстрактный) теперь является экземпляром и является допустимым аргументом для Worker(...), хотя это не имеет смысла, поскольку не предоставляет атрибут x.

Вопрос ...

Теперь я застрял в вопросе о том, как передать этот интерфейс средству проверки типов без использования наследования (из-за конфликта метаклассов; даже если бы это было возможно, mypy все равно жаловался бы на несовместимость подписи x). Есть способ сделать это?


person a_guest    schedule 11.10.2019    source источник


Ответы (3)


Поддержка структурных подтипов была добавлена ​​PEP 544 - Протоколы: структурное подтипирование (статическая утиная типизация), начиная с Python 3.8. Для версий до 3.8 соответствующая реализация предоставляется пакетом typing-extensions на PyPI. .

Для обсуждаемого сценария имеет значение typing.Protocol, как объяснено в PEP. более подробно. Это позволяет определять неявные подтипы, что избавляет нас от конфликта метаклассов. вопрос, так как наследование не требуется. Итак, код выглядит так:

from typing import Protocol             # Python 3.8+
from typing_extensions import Protocol  # Python 3.5 - 3.7


class Fooish(Protocol):
    x : int

    def foo(self) -> int:
        raise NotImplementedError


# No inheritance required, implementing the defined protocol implicitly subtypes 'Fooish'.
class Foo:
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass


class MyMeta(type):
    pass


class Bar(metaclass=MyMeta):
    def bar(self):
        pass


# Here, we again create an implicit subtype of 'Fooish'.
class FooBar(Bar):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    @x.setter
    def x(self, val):
        pass

    def foo(self):
        pass


class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x
person a_guest    schedule 13.10.2019
comment
Это очень хорошо! Когда я впервые увидел ваш вопрос, я подумал о протоколах (слышал о них раньше), но у меня не было с ними практики, поэтому я не использовал их и написал что-то, чтобы обойти конфликт метаклассов :) Но они действительно наиболее подходят для вашего кейс. Узнал что-то новое для себя из вашего ответа, спасибо! - person sanyassh; 13.10.2019
comment
Кроме того, использование протоколов устраняет проблему с x: int в Fooish и свойством в подклассе, которое ранее давало error: Signature of "x" incompatible with supertype "Fooish" с помощью mypy. - person sanyassh; 13.10.2019
comment
@sanyash На самом деле ошибка несовместимости была исправлена ​​добавлением @x.setter в FooBar (что я и сделал в приведенном выше примере кода). Удаление установщика и выполнение Worker(FooBar()) дает ошибку несовместимости вместе со следующим примечанием: Член протокола Fooish.x ожидает устанавливаемую переменную, получил атрибут только для чтения. - person a_guest; 14.10.2019
comment
Если я использую свой код метакласса с x: int в Fooish и с x.getter и x.setter в FooBar, я получаю error: Signature of "x" incompatible with supertype "Fooish". Таким образом, сеттер, похоже, не помогает в этом случае. - person sanyassh; 14.10.2019
comment
@sanyash Действительно, я только что просмотрел ответ на тот другой вопрос, на который вы указали ссылку, и он относится к ошибке в mypy. Так что в целом он должен работать, добавив сеттер. - person a_guest; 14.10.2019

  1. Чтобы избавиться от error: Signature of "x" incompatible with supertype "Fooish", вы можете аннотировать x: typing.Any.
  2. Чтобы сделать Fooish действительно абстрактным, необходимы некоторые уловки для разрешения конфликта метаклассов. Я взял рецепт из этого ответа:
class MyABCMeta(MyMeta, abc.ABCMeta):
    pass

После этого можно создать Fooish:

class Fooish(metaclass=MyABCMeta):

Весь код, который успешно выполняется во время выполнения и не показывает ошибок от mypy:

import abc
import typing

class MyMeta(type):
    pass

class MyABCMeta(abc.ABCMeta, MyMeta):
    pass

class Fooish(metaclass=MyABCMeta):
    x : typing.Any

    @abc.abstractmethod
    def foo(self) -> int:
        raise NotImplementedError

class Bar(metaclass=MyMeta):
    def bar(self):
        pass

class Foo(Fooish):
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass

class Worker:
    def __init__(self, obj: Fooish):
        self.x = obj.x


class FooBar(Bar, Fooish):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    def foo(self):
        pass

print(Worker(FooBar()))

Теперь пора подумать, действительно ли вы хотите сделать Fooish абстрактным, потому что выполнение class Fooish(metaclass=MyABCMeta): может иметь побочные эффекты, если MyMeta выполняет много трюков. Например, если MyMeta определяет __new__, вы, вероятно, можете определить __new__ в Fooish, который не вызывает MyMeta.__new__, а вызывает abc.ABCMeta.__new__. Но все может усложниться ... Так что, может быть, будет проще иметь неабстрактный Fooish.

person sanyassh    schedule 11.10.2019
comment
Использование x: Any в интерфейсе отменяет использование здесь подсказок типов. Тип этих атрибутов важен для Worker, и я хочу, чтобы их тип был проверен, поэтому любой класс, реализующий Fooish, должен предоставлять x: int. Использование x: Any не сообщает об этом ни средству проверки типов, ни разработчикам. Я согласен с тем, что использование ABC с наследованием превращается в беспорядок, особенно из-за смеси ABC. Вот почему я пробовал использовать ABC.register. Но использование обычного класса в качестве миксина интерфейса оставляет модуль с тем экземпляром типа миксина, который даже передает проверки типа для Worker(...). - person a_guest; 12.10.2019
comment
@a_guest Я согласен с тем, что использование x: typing.Any тратит впустую информацию о том, что x имеет тип int. Задал отдельный вопрос по этому поводу: stackoverflow.com/questions/58349417/. - person sanyassh; 12.10.2019
comment
@a_guest может реализовать Fooish как обычный класс, но с def __init__(self, *args, **kwargs): raise RuntimeError('Cant instanciate Fooish') будет работать? - person sanyassh; 12.10.2019
comment
@a_guest получил здесь хороший ответ: stackoverflow.com/a/58349527/9609843. TL; DR: в Fooish вместо x: int реализовать свойство (возможно, абстрактное) с помощью геттера и сеттера. - person sanyassh; 12.10.2019
comment
Действительно, спасибо за указатель. Пытаясь сделать примерный код как можно более компактным, я полностью упустил асимметрию w.r.t. получение / установка атрибута x. Проведя еще несколько исследований, я только что обнаружил, что в PEP 544 добавлена ​​поддержка структурных подтипов, допускающих неявные подтипы (которые не требуют наследования). Я резюмировал это в отдельном ответе, если вам интересно. - person a_guest; 13.10.2019

Если я понимаю, вы могли бы добавить Union, что в основном позволяет Foo or Bar or Fooish:

from typing import Union

class Worker:
    def __init__(self, obj: Union[Bar, Fooish]):
        self.x = obj.x

# no type error
Worker(FooBar())

Со следующим:

class MyMeta(type):
    pass

class Fooish:
    x: int
    def foo(self) -> int:
        raise NotImplementedError


class Bar(metaclass=MyMeta):
    def bar(self):
        pass


class Foo:
    def __init__(self, x: int):
        self.x = x

    def foo(self):
        pass


class Worker:
    def __init__(self, obj: Union[Bar, Fooish]):
        self.x = obj.x


class FooBar(Bar, Fooish):
    """Objects of this type are bar and they are foo-ish."""

    @property
    def x(self) -> int:
        return 0

    def foo(self):
        pass

видеть:

person jmunsch    schedule 11.10.2019
comment
Я тоже думал об использовании Union, но скорее как Fooish = Union[Foo, FooBar]. Однако проблема состоит в том, что все возможные типы должны быть известны во время синтаксического анализа модуля. Поскольку пакет действует как фреймворк, ожидается, что разработчики создадут свои собственные Foo-ish типы, но регистрация их для работы с Worker тогда невозможна (или это? Может быть, с некоторым рефакторингом модулей и определенными порядками импорта? ). Таким образом, пользователи будут получать предупреждения типа на своей стороне. Я быстро проверил проблему, которую вы связали, и это действительно похоже на то, что это может помочь, так что, возможно, мне придется подождать. - person a_guest; 12.10.2019
comment
Проведя еще несколько исследований, я только что обнаружил, что в PEP 544 добавлена ​​поддержка структурных подтипов, допускающих неявные подтипы (которые не требуют наследования). Я резюмировал это в отдельном ответе, если вам интересно. - person a_guest; 13.10.2019