Python - принудительно использовать сигнатуру определенного метода для подклассов? - PullRequest
0 голосов
/ 23 марта 2019

Я хотел бы создать класс, который определяет определенный интерфейс, а затем потребовать, чтобы все подклассы соответствовали этому интерфейсу. Например, я хотел бы определить класс

class Interface:
    def __init__(self, arg1):
       pass

    def foo(self, bar):
       pass

, а затем будьте уверены, что если я держу какой-либо элемент a, имеющий тип A, подкласс Interface, то я могу позвонить a.foo(2), он будет работать.

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

В идеале то, что я ищу, это что-то похожее на Traits и Impls из Rust, где я могу указать конкретную черту и список методов, которые необходимо определить, и тогда я могу быть уверен, что любой объект с этой чертой имеет определенные методы.

Есть ли способ сделать это в Python?

Ответы [ 2 ]

1 голос
/ 23 марта 2019

Итак, во-первых, просто констатирую очевидное: в Python есть встроенный механизм для проверки существования методов и атрибутов в производных классах - он просто не проверяет их подпись.

Во-вторых, приятно посмотреть на пакет zope.interface. Отбросьте пространство имен zope, это полный автономный пакет, который позволяет действительно аккуратные методы иметь объекты, которые могут предоставлять несколько интерфейсов, но только когда это необходимо, и затем освобождает пространства имен. Это, конечно, требует некоторого обучения, пока вы не привыкнете к нему, но оно может быть довольно мощным и обеспечивать очень хорошие шаблоны для больших проектов.

Он был разработан для Python 2, когда в Python было намного меньше возможностей, чем в настоящее время - и я думаю, что он не выполняет автоматическую проверку интерфейса (нужно вручную вызывать метод, чтобы выяснить, соответствует ли класс), - но тем не менее, автоматизировать этот вызов будет легко.

В-третьих, связанный принятый ответ на Как применить сигнатуру метода для дочерних классов? почти работает и может быть достаточно хорош только с одним изменением. Проблема с этим примером заключается в том, что он жестко кодирует вызов type для создания нового класса и не передает type.__new__ информацию о самом метаклассе. Заменить строку:

return type(name, baseClasses, d)

для:

return super().__new__(cls, name, baseClasses, d)

И затем, сделайте, чтобы базовый класс - тот, который определяет ваши требуемые методы, использовал метакласс - он будет обычно наследоваться любыми подклассами. (просто используйте синтаксис Python 3 для указания метаклассов).

Извините - этот пример - Python 2 - он также требует изменения в другой строке, я лучше перепостил:

from types import FunctionType

# from https://stackoverflow.com/a/23257774/108205
class SignatureCheckerMeta(type):
    def __new__(mcls, name, baseClasses, d):
        #For each method in d, check to see if any base class already
        #defined a method with that name. If so, make sure the
        #signatures are the same.
        for methodName in d:
            f = d[methodName]
            for baseClass in baseClasses:
                try:
                    fBase = getattr(baseClass, methodName)

                    if not inspect.getargspec(f) == inspect.getargspec(fBase):
                        raise BadSignatureException(str(methodName))
                except AttributeError:
                    #This method was not defined in this base class,
                    #So just go to the next base class.
                    continue

        return super().__new__(mcls, name, baseClasses, d)

При рассмотрении этого я вижу, что в нем нет механизма, обеспечивающего реализацию метода на самом деле . То есть если метод с тем же именем существует в производном классе, его сигнатура применяется, но если он вообще не существует в производном классе, приведенный выше код не узнает об этом (и метод в суперклассе будет звонил - это может быть желаемое поведение).

Ответ:

Четвертый - Хотя это будет работать, это может быть немного грубым - поскольку он любой метод, который переопределяет другой метод в любом суперклассе, должен будет соответствовать его сигнатуре. И даже совместимые подписи сломались бы. Возможно, было бы неплохо использовать механизмы ABCMeta и @abstractmethod existind, поскольку они уже работают во всех случаях. Однако обратите внимание, что этот пример основан на приведенном выше коде, и проверьте подписи во время создания class , в то время как механизм abstractclass в Python заставляет его проверять, когда создается экземпляр класса. Оставляя это нетронутым, вы сможете работать с большой иерархией классов, которая может содержать некоторые абстрактные методы в промежуточных классах, и только конечные, конкретные классы должны реализовывать все методы. Просто используйте это вместо ABCMeta в качестве метакласса для ваших классов интерфейса и пометьте методы, которые вы хотите проверить, как @abstractmethod как обычно.

class M(ABCMeta):
    def __init__(cls, name, bases, attrs):
        errors = []
        for base_cls in bases:
            for meth_name in getattr(base_cls, "__abstractmethods__", ()):
                orig_argspec = inspect.getfullargspec(getattr(base_cls, meth_name))
                target_argspec = inspect.getfullargspec(getattr(cls, meth_name))
                if orig_argspec != target_argspec:
                    errors.append(f"Abstract method {meth_name!r}  not implemented with correct signature in {cls.__name__!r}. Expected {orig_argspec}.")
        if errors: 
            raise TypeError("\n".join(errors))
        super().__init__(name, bases, attrs)
1 голос
/ 23 марта 2019

Вы можете следовать шаблону pyspark, где метод базового класса выполняет (необязательную) проверку правильности аргумента, а затем вызывает «не публичный» метод подкласса, например:

class Regressor():

    def fit(self, X, y):
        self._check_arguments(X, y)
        self._fit(X, y)

    def _check_arguments(self, X, y):
        if True:
             pass

        else:
            raise ValueError('Invalid arguments.')

class LinearRegressor(Regressor):

    def _fit(self, X, y):
        # code here
...