Create Your First Metaclass

Beginner

This tutorial is from open-source community. Access the source code

Introduction

In this lab, you will learn about metaclasses in Python. In Python, everything, including classes, is an object. A metaclass is a class that creates other classes, offering a powerful way to customize class creation.

The objectives of this lab are to understand what a metaclass is, create your first metaclass, use it to create new classes, and observe how metaclasses affect class inheritance. The file mymeta.py will be created during the lab.

Understanding Metaclasses

Metaclasses are an advanced but powerful feature in Python. As a beginner, you might be wondering what metaclasses are and why they're important. Before we start creating our first metaclass, let's take a moment to understand these concepts.

What is a Metaclass?

In Python, everything is an object, and that includes classes. Just as a regular class is used to create instances, a metaclass is used to create classes. By default, Python uses the built - in type metaclass to create all classes.

Let's break down the class creation process step by step:

  1. First, Python reads the class definition you've written in your code. This is where you define the name of the class, its attributes, and methods.
  2. Then, Python collects important information about the class, such as the class name, any base classes it inherits from, and all of its attributes.
  3. After that, Python passes this collected information to the metaclass. The metaclass is responsible for taking this information and creating the actual class object.
  4. Finally, the metaclass creates and returns the new class.

A metaclass gives you the power to customize this class creation process. You can modify or inspect classes as they are being created, which can be very useful in certain scenarios.

Let's visualize this relationship to make it easier to understand:

Metaclass → creates → Class → creates → Instance

In this lab, we'll create our own metaclass. By doing so, you'll be able to see this class creation process in action and gain a better understanding of how metaclasses work.

Creating Your First Metaclass

Now, we're going to create our very first metaclass. Before we start coding, let's understand what a metaclass is. In Python, a metaclass is a class that creates other classes. It's like a blueprint for classes. When you define a class in Python, Python uses a metaclass to create that class. By default, Python uses the type metaclass. In this step, we'll define a custom metaclass that prints information about the class it's creating. This will help us understand how metaclasses work under the hood.

  1. Open VSCode in the WebIDE and create a new file called mymeta.py in the /home/labex/project directory. This is where we'll write our code for the metaclass.

  2. Add the following code to the file:

## mymeta.py

class mytype(type):
    @staticmethod
    def __new__(meta, name, bases, __dict__):
        print("Creating class :", name)
        print("Base classes   :", bases)
        print("Attributes     :", list(__dict__))
        return super().__new__(meta, name, bases, __dict__)

class myobject(metaclass=mytype):
    pass

Let's break down what this code does:

  • First, we define a new class named mytype that inherits from type. Since type is the default metaclass in Python, by inheriting from it, we're creating our own custom metaclass.
  • Next, we override the __new__ method. In Python, the __new__ method is a special method that's called when creating a new object. In the context of a metaclass, it's called when creating a new class.
  • Inside our __new__ method, we print some information about the class being created. We print the name of the class, its base classes, and its attributes. After that, we call the parent's __new__ method using super().__new__(meta, name, bases, __dict__). This is important because it actually creates the class.
  • Finally, we create a base class named myobject and specify that it should use our custom metaclass mytype.

The __new__ method takes the following parameters:

  • meta: This refers to the metaclass itself. In our case, it's mytype.
  • name: This is the name of the class that's being created.
  • bases: This is a tuple containing the base classes that the new class inherits from.
  • __dict__: This is a dictionary that contains the attributes of the class.
  1. Save the file by pressing Ctrl+S or by clicking File > Save. Saving the file ensures that your code is preserved and can be run later.

Using Your Metaclass

Now, we're going to create a class that uses our metaclass through inheritance. This will help us understand how the metaclass is called when the class is defined.

A metaclass in Python is a class that creates other classes. When you define a class, Python uses a metaclass to construct that class object. By using inheritance, we can specify which metaclass a class should use.

  1. Open mymeta.py and add the following code at the end of the file:
class Stock(myobject):
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Here, we're defining a Stock class that inherits from myobject. The __init__ method is a special method in Python classes. It's called when an object of the class is created and is used to initialize the object's attributes. The cost method calculates the total cost of the stock, and the sell method reduces the number of shares.

  1. Save the file by pressing Ctrl+S. Saving the file ensures that the changes you've made are stored and can be run later.

  2. Now let's run the file to see what happens. Open a terminal in the WebIDE and run:

cd /home/labex/project
python3 mymeta.py

The cd command changes the current working directory to /home/labex/project, and python3 mymeta.py runs the Python script mymeta.py.

You should see output similar to this:

Creating class : myobject
Base classes   : ()
Attributes     : ['__module__', '__qualname__', '__doc__']
Creating class : Stock
Base classes   : (<class '__main__.myobject'>,)
Attributes     : ['__module__', '__qualname__', '__init__', 'cost', 'sell', '__doc__']

This output shows that our metaclass is being invoked when both myobject and Stock classes are created. Notice how:

  • For Stock, the base classes include myobject because Stock inherits from myobject.
  • The attributes list includes all the methods we defined (__init__, cost, sell) along with some default attributes.
  1. Let's interact with our Stock class. Create a new file named test_stock.py with the following content:
## test_stock.py
from mymeta import Stock

## Create a new Stock instance
apple = Stock("AAPL", 100, 154.50)

## Use the methods
print(f"Stock: {apple.name}, Shares: {apple.shares}, Price: ${apple.price}")
print(f"Total cost: ${apple.cost()}")

## Sell some shares
apple.sell(10)
print(f"After selling 10 shares: {apple.shares} shares remaining")
print(f"Updated cost: ${apple.cost()}")

In this code, we're importing the Stock class from the mymeta module. Then we create an instance of the Stock class named apple. We use the methods of the Stock class to print information about the stock, calculate the total cost, sell some shares, and then print the updated information.

  1. Run this file to test our Stock class:
python3 test_stock.py

You should see output like:

Creating class : myobject
Base classes   : ()
Attributes     : ['__module__', '__qualname__', '__doc__']
Creating class : Stock
Base classes   : (<class 'mymeta.myobject'>,)
Attributes     : ['__module__', '__qualname__', '__init__', 'cost', 'sell', '__doc__']
Stock: AAPL, Shares: 100, Price: $154.5
Total cost: $15450.0
After selling 10 shares: 90 shares remaining
Updated cost: $13905.0

Notice that our metaclass information is printed first, followed by the output from our test script. This is because the metaclass is invoked when the class is defined, which happens before the code in the test script is executed.

Exploring Metaclass Inheritance

Metaclasses have a fascinating characteristic: they are "sticky". This means that once a class uses a metaclass, all its subclasses in the inheritance hierarchy will also use the same metaclass. In other words, the metaclass property propagates through the inheritance chain.

Let's see this in action:

  1. First, open the mymeta.py file. At the end of this file, we're going to add a new class. This class, named MyStock, will inherit from the Stock class. The __init__ method is used to initialize the object's attributes, and we call the __init__ method of the parent class using super().__init__ to initialize the common attributes. The info method is used to return a formatted string with information about the stock. Add the following code:
class MyStock(Stock):
    def __init__(self, name, shares, price, category):
        super().__init__(name, shares, price)
        self.category = category

    def info(self):
        return f"{self.name} ({self.category}): {self.shares} shares at ${self.price}"
  1. After adding the code, save the mymeta.py file. Saving the file ensures that the changes we made are stored and can be used later.

  2. Now, we'll create a new file named test_inheritance.py to test the inheritance behavior of the metaclass. In this file, we'll import the MyStock class from the mymeta.py file. Then, we'll create an instance of the MyStock class, call its methods, and print the results to see how the metaclass works through inheritance. Add the following code to test_inheritance.py:

## test_inheritance.py
from mymeta import MyStock

## Create a MyStock instance
tech_stock = MyStock("MSFT", 50, 305.75, "Technology")

## Test the methods
print(tech_stock.info())
print(f"Total cost: ${tech_stock.cost()}")

## Sell some shares
tech_stock.sell(5)
print(f"After selling: {tech_stock.shares} shares remaining")
print(f"Updated cost: ${tech_stock.cost()}")
  1. Finally, run the test_inheritance.py file to see the metaclass in action through inheritance. Open your terminal, navigate to the directory where the test_inheritance.py file is located, and run the following command:
python3 test_inheritance.py

You should see output similar to:

Creating class : myobject
Base classes   : ()
Attributes     : ['__module__', '__qualname__', '__doc__']
Creating class : Stock
Base classes   : (<class 'mymeta.myobject'>,)
Attributes     : ['__module__', '__qualname__', '__init__', 'cost', 'sell', '__doc__']
Creating class : MyStock
Base classes   : (<class 'mymeta.Stock'>,)
Attributes     : ['__module__', '__qualname__', '__init__', 'info', '__doc__']
MSFT (Technology): 50 shares at $305.75
Total cost: $15287.5
After selling: 45 shares remaining
Updated cost: $13758.75

Notice that even though we didn't explicitly specify a metaclass for the MyStock class, the metaclass is still applied. This clearly demonstrates how metaclasses propagate through inheritance.

Practical Uses of Metaclasses

In our example, the metaclass simply prints information about classes. However, metaclasses have many practical applications in real - world programming:

  1. Validation: You can use metaclasses to check if a class has the required methods or attributes. This helps ensure that classes meet certain criteria before they are used.
  2. Registration: Metaclasses can automatically register classes in a registry. This is useful when you need to keep track of all the classes of a certain type.
  3. Interface enforcement: They can be used to ensure that classes implement required interfaces. This helps maintain a consistent structure in your code.
  4. Aspect - oriented programming: Metaclasses can add behaviors to methods. For example, you can add logging or performance monitoring to methods without modifying the method code directly.
  5. ORM systems: In Object - Relational Mapping (ORM) systems like Django or SQLAlchemy, metaclasses are used to map classes to database tables. This simplifies database operations in your application.

Metaclasses are very powerful, but they should be used sparingly. As Tim Peters (famous for the Zen of Python) once said, "Metaclasses are deeper magic than 99% of users should ever worry about."

Summary

In this lab, you have learned what metaclasses are and how they function in Python. You successfully created your first custom metaclass to monitor class creation and used it to generate new classes. Additionally, you observed how metaclasses propagate through inheritance hierarchies.

Metaclasses are an advanced Python feature that offers control over class creation. Although you may not create metaclasses daily, understanding them provides deeper insights into Python's object system and unlocks powerful possibilities for framework and library development. To learn more, explore the official Python documentation and advanced Python books on metaprogramming.