Python Check If Class Implements Unrelated Interface

Viewed 1452

I know it's possible to check if a class implements another if they are actually related.

class A(ABC):

  @abstractmethod
  @property
  def name(self):
    raise NotImplementedException()
    

class B(A):
  
  @property
  def name(self):
    return "B"

implements(B(), A) # Not a real method, what i'd like to exist

But I also want to check if an unrelated class implements another based on the contents of the class.

Class C:
  
  @property
  def name(self):
    return "C"

implements(C(), A) # Not a real method, what i'd like to exist

Is there an easy way to do this or do I need to write a custom function that will compare all of the methods and their method signatures?

2 Answers

Why not use Protocol

and let the runtime_checkable decorator enable the extended checking

from typing import Protocol, runtime_checkable

@runtime_checkable
class A(Protocol):
    @property
    def name(self):
        ...

def implements(proto: Type):
    """ Creates a decorator for classes that checks that the decorated class implements the runtime protocol `proto`
    """
    def _deco(cls_def):
        try:
            assert issubclass(cls_def, proto)
        except AssertionError as e:
            e.args = (f"{cls_def} does not implement protocol {proto}",)
            raise
        return cls_def
    return _deco

@implements(A)
class C:
  def name(self):
    return "C"

Add all the extra type hints as necessary to make sure the constraints are met on params and method return values.

Note: I haven't tried with properties yet

There's a way to accomplish something similar using issubclass() or isinstance() rather than implements(). This works by implementing a new BaseInterface class you can use to derive proper interfaces (that is, types that define a list of methods that need to be implemented, but can't provide a default implementation or be instantiated).

Implementing BaseInterface

First we need a new metaclass for interface-like types. This will provide the desired behaviour of issubclass() and isinstance() and prevent the Interfaces from being instantiated by mistake. At the bottom of the snippet there's an empty BaseInterface class that custom interfaces can be derived from.

class InterfaceMeta(type):
    """A metaclass for defining interfaces.

    An interface defines a set of public methods and attributes needed
    to interact with an object in a particular way, but is never
    instantiated directly.

    """

    def __new__(meta, name, bases, attrs):
        """Prevent interfaces from overriding __new__, __init__, or __call__."""
        banned_methods = ('__new__', '__init__', '__call__')
        if any([banned in attrs for banned in banned_methods]):
            # Someone has tried to make it possible to instantiate an Interface
            raise AttributeError(
                'It is illegal to instantiate an Interface. '
                'Therefore, it is illegal to implement any of the '
                'following methods: {}'.format(', '.join(banned_methods))
            )
        else:
            return super().__new__(meta, name, bases, attrs)

    def __call__(cls, *args, **kwargs):
        """Prevent interfaces from being instantiated."""
        raise NotImplementedError("Interface cannot be instantiated.")

    def __instancecheck__(cls, instance):
        """Control the behaviour of `isinstance`.
        
        An object is considered to be an instance of `cls` if it has
        all of `cls`'s public attributes and methods.

        """
        for cls_attr in dir(cls):
            if cls_attr.startswith('_'):
                # Private and protected attributes are not considered
                # to be part of the interface.
                continue

            if not hasattr(instance, cls_attr):
                # If `instance` is missing a public attribute of `cls`,
                # it is not an instance of `cls`.
                return False
            elif (
                callable(getattr(cls, cls_attr))
                and not callable(getattr(instance, cls_attr))
            ):
                # If `instance` is missing a public method of `cls`,
                # it is not an instance of `cls`.
                return False

        return True
        

class BaseInterface(metaclass=InterfaceMeta):
    """Base class for all interfaces."""
    pass

Example usage

Now we can define an custom interface by inheriting from BaseInterface.

class SomeInterface(BaseInterface):
    """A class defining an interface.

    Other objects that have the same public methods and attributes 
    as `SomeInterface` are considered to be instances of `SomeInterface`.

    """

    def name(self):
        pass

    def age(self):
        pass


class Foo:
    def name(self):
        # Some implementation...
        pass

    def age(self):
        # Some implementation...
        pass

Since Foo implements all the public methods of SomeInterface, we get the following behaviour.

In [2]: issubclass(Foo(), SomeInterface)
Out[2]: True

In [3]: isinstance(Foo(), SomeInterface)
Out[3]: True

More details here.

Related