Как заставить подкласс реализовывать абстрактные методы родительского класса, используя __init_subclass__ вместо ABCMeta? - PullRequest
0 голосов
/ 30 января 2019

У меня есть следующий код для сравнения текущей (пустой) реализации базовых классов требуемых функций с ее подклассами, которые должны реализовывать их некоторым другим способом, чтобы считаться приемлемыми во время выполнения.Без использования metaclass=ABCMeta и реализации @abstractmethod декораторов для этих методов базового класса, как мне это сделать?На данный момент я пишу следующую __init_subclass__ ловушку для моих специальных, не содержащих метаклассов абстрактных базовых классов в нескольких местах в моем проекте, но это не так.

import inspect

class AbstractThing:
    def __init__(self, topic: str, thing: Thing):
        thing.subscriptions[topic] = self.on_message
        thing.on_connected.append(self.on_connected)
        thing.on_disconnected.append(self.on_disconnected)

    def __init_subclass__(cls):
        required_methods = ['on_connected', 'on_disconnected', 'on_message']
        for f in required_methods:
            func_source = inspect.getsourcelines(getattr(cls, f))
            # if this class no longer inherits from `Object`, the method resolution order will have updated
            parent_func_source = inspect.getsourcelines(getattr(cls.__mro__[-2], f))
            if func_source == parent_func_source:
                raise NotImplementedError(f"You need to override method '{f}' in your class {cls.__name__}")

    def on_connected(self, config: dict):
        pass

    def on_disconnected(self):
        pass

    def on_message(self, msg: str):
        pass

Есть ли лучшеспособ сделать это?Бонусные баллы, если я могу получить ошибки проверки типов в моем редакторе при определении подклассов этого AbstractThing.

1 Ответ

0 голосов
/ 06 февраля 2019

Действительно, вы не должны полагаться на inspect.getsourcelines для любого кода, который должен использоваться в серьезных контекстах (т. Е. Вне области эксперимента, или инструментов для работы с самим исходным кодом)

Простая и простаяОператора is достаточно, чтобы проверить, является ли метод в данном классе таким же, как в базовом классе.(В Python 3. Пользователи Python 2 должны позаботиться о том, чтобы методы извлекались как unbound methods вместо raw-функций)

Кроме этого, вы делаете несколько ненужных ходов, чтобы добраться до базового классасама по себе - мало документированная и мало используемая специальная переменная __class__ может помочь вам в этом: это автоматическая ссылка на тело класса, в котором она написана (не путайте с self.__class__, который является ссылкойвместо этого в подкласс).

Из документации:

Это объект класса, на который будет ссылаться форма с нулевым аргументом super(). __class__, неявнаяссылка на замыкание, созданная компилятором, если какие-либо методы в теле класса ссылаются на __class__ или super. Это позволяет форме нулевого аргумента super() правильно идентифицировать определяемый класс на основе лексической области видимости, тогда как класс или экземпляр, который использовался для выполнения текущего вызова, идентифицируется на основе первого аргумента, переданного вметод.

Таким образом, при сохранении вашего основного подхода все может быть гораздо проще:

def __init_subclass__(cls):
    required_methods = ['on_connected', 'on_disconnected', 'on_message']
    for f in required_methods:
         if getattr(cls, f) is getattr(__class__, f):
              raise NotImplementedError(...)

Если у вас сложная иерархия, и у вас будут родительские классы сдругие обязательные методы, которые должны быть реализованы подклассами тех, и, следовательно, не могут жестко закодировать необходимые методы в required_methods, вы все равно можете использовать декоратор abstractmethod из abc, без использования ABCMetaметаклассом.Все, что делает декоратор, - это создает атрибут метода, который проверяется в метаклассе.Просто выполните ту же проверку в методе __init_subclass__:

from abc import abstractmethod

class Base:
   def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        for attr_name in dir(cls):
            method = getattr(cls, attr_name)
            if (getattr(method, '__isabstractmethod__', False) and
                    not attr_name in cls.__dict__):
                # The second condition above allows 
                # abstractmethods to exist in the class where 
                # they are defined, but not on further subclasses
                raise NotImplementedError(...)

class NetworkMixin(Base):
    @abstractmethod
    def on_connect(self):
         pass

class FileMixin(Base):
    @abstractmethod
    def on_close(self):
         pass

class MyFileNetworkThing(NetworkMixin, FileMixin):
    # if any of the two abstract methods is not
    # implemented, Base.__init_subclass__ will fail

Имейте в виду, что это просто проверка методов, которые отображаются в классе 'dir.Но настройка __dir__ используется достаточно редко, чтобы быть надежной - просто позаботьтесь об этом.

...