1

One of the answers to a previous question I asked suggests the use of a metaclass.

class DogType(type):

    def __init__(cls, name, bases, attrs):
        """ this is called at the Dog-class creation time.  """

        if not bases:
            return

        #pick the habits of direct ancestor and extend it with 
        #this class then assign to cls.
        if "habits" in attrs:
            base_habits = getattr(bases[0], "habits", [])
            cls.habits = base_habits + cls.habits


class Dog(metaclass=DogType):
    habits = ["licks butt"]

    def __repr__(self):
        return f"My name is {self.name}.  I am a {self.__class__.__name__} %s and I like to {self.habits}"

    def __init__(self, name):
        """ dog instance can have all sorts of instance variables"""
        self.name = name

class Sheperd(Dog):
    habits = ["herds sheep"]

class GermanSheperd(Sheperd):
    habits = ["bites people"]

class Poodle(Dog):
    habits = ["barks stupidly"]

class StBernard(Dog):
    pass


for ix, cls in enumerate([GermanSheperd, Poodle, StBernard]):
    name = f"dog{ix}"
    dog = cls(name)
    print(dog)

However this throws an error:

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

I like this solution, however I also really need class Dog to behave like a metaclass such that I can define abstract methods in Dog that will need to propagate in all subclasses. This could be a method like def bark() which all sub-dogs would need to implement...

How do I get Dog to be both a metaclass implementing the functionality in DogType, but also an Abstract class of its own accord which restricts how subclasses are instantiated and run?

user32882
  • 5,094
  • 5
  • 43
  • 82
  • do you simply want to apply a metaclass to an ABC class? – Mia Jun 04 '20 at 07:30
  • Perhaps.... could you please elaborate on that? – user32882 Jun 04 '20 at 07:32
  • 1
    i'm a bit confused on what you want to achieve since your sample code has no method or attribute, but it seems `ABCMeta` is what you need. – Mia Jun 04 '20 at 07:35
  • Can you please clarify what you actually intend to *do*? Metaclasses can completely redefine class behaviour, so it is very important to know what behaviour is actually desired. Can you give an example of what the metaclass or class should provide? Can you rework the description? There is no multiple-inheritance in your code, and the ``MetaClass`` does nothing. The simplest "solution" is just not to use ``MetaClass`` at all. – MisterMiyagi Jun 04 '20 at 08:10
  • What I need to do is basically what is in the previous linked question. In any case I have reformatted the entire question. Perhaps it is clearer now? Please let me know if it is not... – user32882 Jun 04 '20 at 08:19
  • I do not see how this makes the answer by @pkqxdd not applicable (your comments suggest it is not applicable, but without a clear reason). It seems that you are confusing abstract base and meta classes – the abstract *base* class actually defines which methods concrete classes must provide. The "abstract *meta* class" (``ABCMeta``) merely implements the framework to define this – it is not abstract itself. Are you aware that ``__init_subclass__ `` on a regular ``abc.ABC`` subclass, as suggested in the highest-upvoted answer, is significantly simpler and provides the desired behaviour as well? – MisterMiyagi Jun 04 '20 at 08:27
  • Thanks for your comment @MisterMiyagi it makes things a bit clearer. I was able to get what I want as indicated by the answer below. I think I prefer the slightly less upvoted question in my previous question as it has allowed me a slightly better understanding of how all this stuff works... – user32882 Jun 04 '20 at 08:42

2 Answers2

1

If you look at the source code for the ABC class, you'll find that it's a simple instance of the ABCMeta class, which is why your example gave a metaclass conflict Thus, from your example, you can achieve it by

from abc import ABCMeta, abstractmethod

class Meta(ABCMeta):
    pass

class BaseClass(metaclass=Meta):
    @abstractmethod
    def something(self):
        pass

class DerivedClass(BaseClass):
    def something(self):
        return 1

try:
    BaseClass()
except TypeError:
    pass
else:
    raise Exception('Meta class failed')

DerivedClass()

And you can see that this program runs just fine.

Mia
  • 2,466
  • 22
  • 38
  • I think I may have given my classes confusing names in the original question. I have just edited the class names in the original question, please take a look. – user32882 Jun 04 '20 at 07:53
  • I need the metaclass to define how abstract base class implementations will behave, not the other way round. I may have other abstract base classes based on this same metaclass – user32882 Jun 04 '20 at 07:55
  • 1
    @user32882 So it seems that the answer I provided should work since it's pretty much the same as the standard ABC class? What specific behavior do you want to achieve/modify? – Mia Jun 04 '20 at 08:10
0

@MisterMiyagi's comments have provided some food for thought and also allowed me to better understand how ABC's themselves are implemented. It seems the "Abstract to Concrete" order is something like this:

type --> ABCMeta --> ABC --> regular class --> regular subclass --> object

This being said, if DogType inherits from ABCMeta as opposed to from type, it can still act as a metaclass, all the while allowing its subclasses to act as abstract base classes, since ABCMeta is the metaclass for any ABC. This allows us to do the following:


class DogType(ABCMeta):

    def __init__(cls, name, bases, attrs):
        """ this is called at the Dog-class creation time.  """

        if not bases:
            return

        #pick the habits of direct ancestor and extend it with
        #this class then assign to cls.
        if "habits" in attrs:
            base_habits = getattr(bases[0], "habits", [])
            cls.habits = base_habits + cls.habits


class Dog(metaclass=DogType):
    habits = ["licks butt"]

    def __repr__(self):
        return f"My name is {self.name}.  I am a {self.__class__.__name__} %s and I like to {self.habits}"

    def __init__(self, name):
        """ dog instance can have all sorts of instance variables"""
        self.name = name

    @abstractmethod
    def print_habits(self):
        for habit in self.habits:
            print(habit)


class Sheperd(Dog):
    habits = ["herds sheep"]
    def print_habits(self):
        for habit in self.habits:
            print(habit)

class GermanSheperd(Sheperd):
    habits = ["bites people"]
    def print_habits(self):
        for habit in self.habits:
            print(habit)

class Poodle(Dog):
    habits = ["barks stupidly"]
    def print_habits(self):
        for habit in self.habits:
            print(habit)

class StBernard(Dog):
    def print_habits(self):
        for habit in self.habits:
            print(habit)


for ix, cls in enumerate([GermanSheperd, Poodle, StBernard]):
    name = f"dog{ix}"
    print('\n', name)
    print(cls)
    dog = cls(name)
    dog.print_habits()

I am aware that it is unclear in the above code why I am defining print_habits as an abstractmethod and reimplementing it in subclasses. I could simply define it once in Dog and it would be just fine, but in my use case there are methods that need to be enforced in all Dog subclasses, and some that don't.

user32882
  • 5,094
  • 5
  • 43
  • 82
  • 1
    Note that meta-classes are orthogonal to subclasses. The relation between ``ABCMeta`` and ``ABC`` is *not* the same as the relation between ``type`` and ``ABCMeta`` or between ``ABC`` and ``regular class``. The former is an ``isinstance`` relation, the others are ``issubclass`` relations. – MisterMiyagi Jun 04 '20 at 08:38
  • I'm just getting the hang of this stuff a bit more... thanks for your useful comments – user32882 Jun 04 '20 at 08:40