user32882
user32882

Reputation: 5877

Want a class that will behave like an ABC but also a metaclass

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?

Upvotes: 1

Views: 61

Answers (2)

user32882
user32882

Reputation: 5877

@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.

Upvotes: 0

Mia
Mia

Reputation: 2676

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.

Upvotes: 1

Related Questions