Metaclasses in Python

Metaclasses, denoted by Metaclasses, are a fascinating concept in object-oriented programming. They are essentially classes that create other classes. While all OOP languages have metaclasses (like Type in C# or Class in Java), the ability to define custom ones, as seen in Python and Groovy, is particularly interesting.

My previous post, so many years ago, delved into Groovy metaclasses. Unlike Groovy, custom metaclasses are less common in Python because features achievable through metaclasses in Groovy often have alternative implementations in Python (e.g., using __getattribute__, __getattr__). However, understanding metaclasses in Python is still valuable, particularly their role in callables and attribute lookup.

For an in-depth look at Python metaclasses, I recommend This. It enhanced my understanding of this complex topic. Let’s break down the key points:

Types:

  • Classes and functions are themselves objects in Python.
  • The base object object is an instance of the type class.
  • type is uniquely an instance of itself.
  • All classes are instances of type.
  • Consequently, metaclasses, being classes that create classes, are also instances of type and inherit from it.
  • To determine a class’s metaclass, use type(A) where A represents your class.

Callables:

  • When Python encounters x(), it determines the class of x.
  • It then calls the __call__ method of that class, passing x as the first argument.

Instance Creation:

  • Creating an instance like a = A() invokes the __call__ method of the class’s metaclass.
  • For regular classes, this is type.__call__. For classes with custom metaclasses, it’s CustomMetaclass.__call__.
  • The process involves type.__call__(cls, *args, **kwargs), which calls cls.__new__(cls) (defaulting to object.__new__ unless overridden) to create the instance, followed by cls.__init__(instance) to initialize it.

Class Creation:

  • Defining a class like class A: is similar to A = type("A", bases, attributes).
  • This invokes type.__call__(type), leading to calls to type.__new__ and type.__init__ to create the class object. (Note: type.__new__ differs from object.__new__, see they are not).

Custom Metaclasses:

  • Using class A(metaclass=MetaB): creates an instance of MetaB, calling MetaB.__new__ and MetaB.__init__.
  • Creating an instance of A (which uses MetaB) with a = A() invokes MetaB.__call__ (which might be overridden or default to type.__call__).

Key Uses of Metaclasses:

  1. Managing class creation: Defining __new__ and/or __init__ in the metaclass controls the creation of classes (instances of the metaclass).
  2. Managing instance creation: Defining a custom __call__ in the metaclass controls the creation of instances of classes that utilize the metaclass (e.g., implementing singletons).

This article provides a good explanation of these concepts. Here’s a code snippet illustrating the signatures:

1
2
3
4
5
6
7
8
class MyMeta(type):
    def __new__(self, name, base, ns):
        print("meta new")
        return type.__new__(self, name, base, ns)
    def __init__(self, name, base, ns):
        print("meta init") 
        self.cloned = False
        type.__init__(self, name, base, ns) 

Metaclasses and Attribute Lookup:

  • Metaclasses don’t add complexity to attribute lookup.
  • For a.at1, Python searches a’s __dict__, then its class (and base classes). The metaclass is not involved.
  • For A.att1, Python searches A’s __dict__, then its base classes, and finally type.__dict__ (since A is an instance of type). The metaclass is referenced indirectly.

Metaclass Inheritance:

  • Custom metaclasses are inherited from base classes.
  • Multiple inheritance scenarios can be complex. superb article explains this well.

Metaclass Immutability:

  • You can’t change a class’s metaclass after creation.
  • However, there’s a workaround: create a new class inheriting from the original and assign the new metaclass.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Meta1(type):
    pass

class A(metaclass=Meta1):
    pass

class Meta2(type):
    pass	

# Change A's metaclass to Meta2
class A2(A, metaclass=Meta2):
    pass

Use A2 to create new instances with the updated metaclass. Existing instances aren’t affected as their metaclass is determined upon instance creation.

This approach avoids modifying existing instances, which aligns with the intended behavior. my previous post demonstrates a similar technique.

Licensed under CC BY-NC-SA 4.0
Last updated on Jun 27, 2023 14:53 +0100