Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

1 class inherits 2 different metaclasses (abcmeta and user defined meta)

Tags:

I have a class1 that needs to inherit from 2 different metaclasses which is Meta1 and abc.ABCMeta

Current implementation:

Implementation of Meta1:

class Meta1(type):
    def __new__(cls, classname, parent, attr):
        new_class = type.__new__(cls, classname, parent, attr)
        return super(Meta1, cls).__new__(cls, classname, parent, attr)

implementation of class1Abstract

class class1Abstract(object):
    __metaclass__ = Meta1
    __metaclass__ = abc.ABCMeta

implementation of mainclass

class mainClass(class1Abstract):
    # do abstract method stuff

I know this is wrong to implement 2 different meta twice.

I change the way metclass is loaded (a few tries) and I get this TypeError: Error when calling the metaclass bases

metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

I ran out of ideas...


EDITED 1

I tried this solution it works but the mainClass is not an instance of class1Abstract

print issubclass(mainClass, class1Abstract) # true
print isinstance(mainClass, class1Abstract) # false

Implementation of class1Abstract

class TestMeta(Meta1):
    pass


class AbcMeta(object):
    __metaclass__ = abc.ABCMeta
    pass


class CombineMeta(AbcMeta, TestMeta):
    pass


class class1Abstract(object):
    __metaclass__ = CombineMeta

    @abc.abstractmethod
    def do_shared_stuff(self):
        pass

    @abc.abstractmethod
    def test_method(self):
        ''' test method '''

Implementation of mainClass

class mainClass(class1Abstract):
    def do_shared_stuff(self):
        print issubclass(mainClass, class1Abstract) # True
        print isinstance(mainClass, class1Abstract) # False

Since mainClass inherits from an abstract class python should complain about test_method not being implemented in mainClass. But it doesn't complain anything because print isinstance(mainClass, class1Abstract) # False

dir(mainClass) doesn't have

['__abstractmethods__', '_abc_cache', '_abc_negative_cache', '_abc_negative_cache_version', '_abc_registry']

HELP!


EDITED 2

Implementation of class1Abstract

CombineMeta = type("CombineMeta", (abc.ABCMeta, Meta1), {})
class class1Abstract(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def do_shared_stuff(self):
        pass

    @abc.abstractmethod
    def test_method(self):
        ''' test method '''

Implementation of mainClass

class mainClass(class1Abstract):
    __metaclass__ = CombineMeta
    def do_shared_stuff(self):
        print issubclass(mainClass, class1Abstract) # True
        print isinstance(mainClass, class1Abstract) # False

dir(mainClass) now have abstractmethod's magic methods

['__abstractmethods__', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__metaclass__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_abc_cache', '_abc_negative_cache', '_abc_negative_cache_version', '_abc_registry', 'do_shared_stuff', 'test_method']

But python doesn't warn about test_method not being instantiated

HELP!

like image 259
momokjaaaaa Avatar asked Jul 13 '15 09:07

momokjaaaaa


People also ask

Are metaclasses inherited?

Every object and class in Python is either an instance of a class or an instance of a metaclass. Every class inherits from the built-in basic base class object , and every class is an instance of the metaclass type .

What is ABCMeta metaclass?

ABCMeta metaclass provides a method called register method that can be invoked by its instance. By using this register method, any abstract base class can become an ancestor of any arbitrary concrete class.

What is meta class in OOP?

In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances. Not all object-oriented programming languages support metaclasses.

What are metaclasses and when are they used in Python?

A metaclass in Python is a class of a class that defines how a class behaves. A class is itself an instance of a metaclass. A class in Python defines how the instance of the class will behave. In order to understand metaclasses well, one needs to have prior experience working with Python classes.


2 Answers

In Python, every class can only have one metaclass, not many. However, it is possible to achieve similar behaviour (like if it would have multiple metaclasses) by mixing what these metaclasses do.

Let's start simple. Our own metaclass, simply adds new attribute to a class:

class SampleMetaClass(type):
  """Sample metaclass: adds `sample` attribute to the class"""
  def __new__(cls, clsname, bases, dct):
    dct['sample'] = 'this a sample class attribute'
    return super(SampleMetaClass, cls).__new__(cls, clsname, bases, dct)

class MyClass(object):
  __metaclass__ = SampleMetaClass

print("SampleMetaClass was mixed in!" if 'sample' in MyClass.__dict__ else "We've had a problem here")

This prints "SampleMetaClass was mixed in!", so we know our basic metaclass works fine.

Now, on the other side, we want an abstract class, in its simplest it would be:

from abc import ABCMeta, abstractmethod

class AbstractClass(object):
  __metaclass__ = ABCMeta
  @abstractmethod
  def implement_me(self):
    pass

class IncompleteImplementor(AbstractClass):
  pass

class MainClass(AbstractClass):
  def implement_me(self):
    return "correct implementation in `MainClass`"

try:
  IncompleteImplementor()
except TypeError as terr:
  print("missing implementation in `IncompleteImplementor`")

MainClass().implement_me()

This prints "missing implementation in IncompleteImplementor" followed up by "correct implementation in MainClass". So, the abstract class also works fine.

Now, we have 2 simple implementations and we need to mix together the behaviour of the two metaclasses. There are multiple options here.

Option 1 - subclassing

One can implement a SampleMetaClass as a subclass of ABCMeta - metaclasses are also classes and one can inhereit them!

class SampleMetaABC(ABCMeta):
  """Same as SampleMetaClass, but also inherits ABCMeta behaviour"""
  def __new__(cls, clsname, bases, dct):
    dct['sample'] = 'this a sample class attribute'
    return super(SampleMetaABC, cls).__new__(cls, clsname, bases, dct)

Now, we change the metaclass in the AbstractClass definition:

class AbstractClass(object):
  __metaclass__ = SampleMetaABC
  @abstractmethod
  def implement_me(self):
    pass

# IncompleteImplementor and MainClass implementation is the same, but make sure to redeclare them if you use same interpreter from the previous test

And run both our tests again:

try:
  IncompleteImplementor()
except TypeError as terr:
  print("missing implementation in `IncompleteImplementor`")

MainClass().implement_me()

print("sample was added!" if 'sample' in IncompleteImplementor.__dict__ else "We've had a problem here")
print("sample was added!" if 'sample' in MainClass.__dict__ else "We've had a problem here")

This would still print that IncompleteImplementor is not correctly implemented, that MainClass is, and that both now have the sample class-level attribute added. Thing to note here that Sample part of the metaclass was successfully applied to the IncompleteImplementor as well (well, there is no reason it wouldn't).

As it would be expected, isinstance and issubclass still work as supposed to:

print(issubclass(MainClass, AbstractClass)) # True, inheriting from AbtractClass
print(isinstance(MainClass, AbstractClass)) # False as expected - AbstractClass is a base class, not a metaclass
print(isinstance(MainClass(), AbstractClass)) # True, now created an instance here

Option 2 - composing metaclasses

In fact, there is this option in the question itself, it only required a small fix. Declare new metaclass as a composition of several simpler metaclasses to mix their behaviour:

SampleMetaWithAbcMixin = type('SampleMetaWithAbcMixin', (ABCMeta, SampleMetaClass), {})

As previously, change the metaclass for an AbstractClass (and again, IncompleteImplementor and MainClass don't change, but redeclare them if in same interpreter):

class AbstractClass(object):
  __metaclass__ = SampleMetaWithAbcMixin
  @abstractmethod
  def implement_me(self):
    pass

From here, running same tests should yield same results: ABCMeta still works and ensures that @abstractmethod-s are implemented, SampleMetaClass adds a sample attribute.

I personally prefer this latter option, for the same reason as I generally prefer composition to the inheritance: the more combinations one eventually needs between multiple (meta)classes - the simpler it would be with composition.

More on metaclasses

Finally, the best explanation of metaclasses I've ever read is this SO answer: What is a metaclass in Python?

like image 198
Timur Avatar answered Oct 14 '22 02:10

Timur


By default python only complains that class has abstract methods when you attempt to instantiate the class, not when you create the class. This is because the class's metaclass is still ABCMeta (or subtype thereof), so it's permitted to have abstract methods.

To get what you want you would need to write your own metaclass that raises an error when it spots that __abstractmethods__ isn't empty. This way you have to explicitly state when a class is no longer allowed abstract methods.

from abc import ABCMeta, abstractmethod

class YourMeta(type):
    def __init__(self, *args, **kwargs):
        super(YourMeta, self).__init__(*args, **kwargs)
        print "YourMeta.__init__"
    def __new__(cls, *args, **kwargs):
        newcls = super(YourMeta, cls).__new__(cls, *args, **kwargs)
        print "YourMeta.__new__"
        return newcls

class ConcreteClassMeta(ABCMeta):
    def __init__(self, *args, **kwargs):
        super(ConcreteClassMeta, self).__init__(*args, **kwargs)
        if self.__abstractmethods__:
            raise TypeError("{} has not implemented abstract methods {}".format(
                self.__name__, ", ".join(self.__abstractmethods__)))

class CombinedMeta(ConcreteClassMeta, YourMeta):
    pass

class AbstractBase(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def f(self):
        raise NotImplemented

try:
    class ConcreteClass(AbstractBase):
        __metaclass__ = CombinedMeta

except TypeError as e:
    print "Couldn't create class --", e

class ConcreteClass(AbstractBase):
    __metaclass__ = CombinedMeta
    def f(self):
        print "ConcreteClass.f"

assert hasattr(ConcreteClass, "__abstractmethods__")
c = ConcreteClass()
c.f()

Which outputs:

YourMeta.__new__
YourMeta.__init__
Couldn't create class -- ConcreteClass has not implemented abstract methods f
YourMeta.__new__
YourMeta.__init__
ConcreteClass.f
like image 42
Dunes Avatar answered Oct 14 '22 04:10

Dunes