Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python - Enforce specific method signature for subclasses?

I would like to create a class which defines a particular interface, and then require all subclasses to conform to this interface. For example, I would like to define a class

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

    def foo(self, bar):
       pass

and then be assured that if I am holding any element a which has type A, a subclass of Interface, then I can call a.foo(2) it will work.

It looked like this question almost addressed the problem, but in that case it is up to the subclass to explicitly change it's metaclass.

Ideally what I'm looking for is something similar to Traits and Impls from Rust, where I can specify a particular Trait and a list of methods that trait needs to define, and then I can be assured that any object with that Trait has those methods defined.

Is there any way to do this in Python?

like image 737
mboratko Avatar asked Dec 17 '22 17:12

mboratko


1 Answers

So, first, just to state the obvious - Python has a built-in mechanism to test for the existence of methods and attributes in derived classes - it just does not check their signature.

Second, a nice package to look at is zope.interface. Despte the zope namespace, it is a complete stand-alone package that allows really neat methods of having objects that can expose multiple interfaces, but just when needed - and then frees-up the namespaces. It sure involve some learning until one gets used to it, but it can be quite powerful and provide very nice patterns for large projects.

It was devised for Python 2, when Python had a lot less features than nowadays - and I think it does not perform automatic interface checking (one have to manually call a method to find-out if a class is compliant) - but automating this call would be easy, nonetheless.

Third, the linked accepted answer at How to enforce method signature for child classes? almost works, and could be good enough with just one change. The problem with that example is that it hardcodes a call to type to create the new class, and do not pass type.__new__ information about the metaclass itself. Replace the line:

return type(name, baseClasses, d)

for:

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

And then, make the baseclass - the one defining your required methods use the metaclass - it will be inherited normally by any subclasses. (just use Python's 3 syntax for specifying metaclasses).

Sorry - that example is Python 2 - it requires change in another line as well, I better repost it:

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)

On reviewing that, I see that there is no mechanism in it to enforce that a method is actually implemented. I.e. if a method with the same name exists in the derived class, its signature is enforced, but if it does not exist at all in the derived class, the code above won't find out about it (and the method on the superclass will be called - that might be a desired behavior).

The answer:

Fourth - Although that will work, it can be a bit rough - since it does any method that override another method in any superclass will have to conform to its signature. And even compatible signatures would break. Maybe it would be nice to build upon the ABCMeta and @abstractmethod existind mechanisms, as those already work all corner cases. Note however that this example is based on the code above, and check signatures at class creation time, while the abstractclass mechanism in Python makes it check when the class is instantiated. Leaving it untouched will enable you to work with a large class hierarchy, which might keep some abstractmethods in intermediate classes, and just the final, concrete classes have to implement all methods. Just use this instead of ABCMeta as the metaclass for your interface classes, and mark the methods you want to check the interface as @abstractmethod as usual.

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)
like image 77
jsbueno Avatar answered Dec 30 '22 04:12

jsbueno