Как реализовать интерфейс, совместимый со статическими проверками типов? - PullRequest
5 голосов
/ 12 октября 2019

У меня есть два базовых класса, 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 в форме «нормального» класса, а затем наследование от него и 1054 *, и 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). Есть ли способ сделать это?

Ответы [ 3 ]

1 голос
/ 12 октября 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.

1 голос
/ 13 октября 2019

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

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

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
0 голосов
/ 12 октября 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

см:

...