Abstract classes using Metaclasses

In Python 3, all classes are new-style classes, making it reasonable to refer to an object’s type and its class interchangeably.

Type and Class

In Python, every class is an instance of a metaclass, and by default, the metaclass is type. This creates an interesting relationship where:

  • Foo (a class) is an instance of the metaclass type.
  • type itself is an instance of type.

Example: Type and Class Relationship

class Foo:
    pass

print(type(Foo))  # Output: <class 'type'>

Here, type is the metaclass that created Foo.

The type Metaclass

The type metaclass is initialized with three arguments:

  1. name: The name of the class (corresponding to the __name__ attribute).
  2. bases: A tuple of base classes the class inherits from.
  3. namespace: A dictionary containing the definitions of the class body (corresponding to the __dict__ attribute).

This mechanism makes metaclasses highly flexible, allowing developers to modify class behavior programmatically.

Creating an Abstract Class Manually with Metaclasses

To understand metaclasses better, let’s manually create an interface (or abstract class) implementation. For production work, you should use from abc import ABC, abstractmethod to implement abstract classes. But for this exercise, we’ll implement the functionality ourselves.

Example: Abstract Class with a Custom Metaclass

# Decorator to mark a function as abstract
def abstract_func(func):
    func.__isabstract__ = True
    return func

# Metaclass inheriting from `type`
class Interface(type):
    def __init__(self, name, bases, namespace):
        print(f"Initializing {name}...")
        class_methods = getattr(self, 'all_methods', [])
        for base in bases:
            required_methods = getattr(base, 'abstract_methods', [])
            for method in required_methods:
                if method not in class_methods:
                    msg = f"""Can't create abstract class {name}!
{name} must implement abstract method {method} of class {base}!"""
                    raise TypeError(msg)

    def __new__(cls, name, bases, namespace):
        namespace['abstract_methods'] = Interface._get_abstract_methods(namespace)
        namespace['all_methods'] = Interface._get_all_methods(namespace)
        return super().__new__(cls, name, bases, namespace)

    @staticmethod
    def _get_abstract_methods(namespace):
        return [name for name, val in namespace.items() if callable(val) and getattr(val, '__isabstract__', False)]

    @staticmethod
    def _get_all_methods(namespace):
        return [name for name, val in namespace.items() if callable(val)]


# Using the custom metaclass
class NetworkInterface(metaclass=Interface):

    @abstract_func
    def connect(self):
        pass

    @abstract_func
    def transfer(self):
        pass

# The following class will raise a TypeError because it does not implement all abstract methods
class TestNetwork(NetworkInterface):
    def __init__(self):
        print("TestNetwork initialized.")

    def connect(self):
        pass

# Uncommenting the next line will raise a TypeError
# c = TestNetwork()

# Correct implementation
class CompleteNetwork(NetworkInterface):
    def connect(self):
        pass

    def transfer(self):
        pass

c = CompleteNetwork()  # This works as all abstract methods are implemented

What Happens During Initialization?

When we attempt to initialize TestNetwork:

  1. The Interface metaclass’s __init__ method is invoked twice:
    • First for the NetworkInterface class.
    • Then for the TestNetwork class.
  2. The metaclass collects the list of abstract methods from the parent (NetworkInterface).
  3. It checks if all abstract methods are implemented in TestNetwork.
  4. If any abstract method is missing, a TypeError is raised.

Key Takeaways

  • Abstract Methods: Marked using a decorator (abstract_func) to indicate required methods in child classes.
  • Dynamic Validation: The metaclass checks for the presence of all required methods during class creation.
  • Flexibility: This implementation shows how metaclasses can control class behavior programmatically.

Using abc.ABC for Real-World Scenarios

In practice, you should use the built-in abc module for abstract base classes. It simplifies abstract class creation and ensures consistency:

from abc import ABC, abstractmethod

class NetworkInterface(ABC):
    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def transfer(self):
        pass

References and Sources

End
Built with Hugo
Theme Stack designed by Jimmy