python-course.eu

17. Road to Metaclasses

By Bernd Klein. Last modified: 24 Mar 2024.

On the Road to Metaclasses

Philosophers on the road to metaclasses

This chapter of our Python tutorial is not about Metaclasses, though it provides the road to metaclasses. This page is about class decoration, which is a powerful concept in object-oriented programming that allows developers to dynamically modify or extend the behavior of classes in Python. It provides a flexible mechanism for adding new functionalities, altering existing methods, or injecting attributes into classes at runtime.

In this chapter, we'll delve into the fundamentals of class decoration. It's helpful to understand the decorator design pattern. You can study this subject in our chapter "Decorators and DEcoration" in Python.

To demonstrate to need for decoration or even metaclasses, we will introduce and design a bunch of philosopher classes. Each philosopher class (Philosopher1, Philosopher2, and so on) need the same "set" of methods (in our example just one, i.e. "the_answer") as the basics for his or her pondering and brooding. Of course you should imagine that the classes have different methods that we have not implemented. A stupid way to implement the classes consists in having the same code for the_answer in every philospher class:

class Philosopher1: 
    
    def the_answer(self, *args):              
        return 42

    
class Philosopher2: 

    def the_answer(self, *args):              
        return 42

    
class Philosopher3: 

    def the_answer(self, *args):              
        return 42

    
plato = Philosopher1()
print(plato.the_answer())

aristotle = Philosopher2()
# let's see what rissstotle has to say :-)
print(aristotle.the_answer())

OUTPUT:

42
42

We can see that we have multiple copies of the method "the_answer". This is error prone and tedious to maintain, of course. Let Plato and Aristotle continue on our way.

Plato and Aristotle

From what we know so far, the easiest way to accomplish our goal without creating redundant code consists in designing a base, which contains "the_answer" as a method. Each Philosopher class inherits now from this base class:

class Answers:

    def the_answer(self, *args):              
        return 42
    

class Philosopher1(Answers): 
    pass


class Philosopher2(Answers): 
    pass


class Philosopher3(Answers): 
    pass


plato = Philosopher1()
print(plato.the_answer())


aristotle = Philosopher2()
# let's see what rissstotle has to say :-)
print(aristotle.the_answer())

OUTPUT:

42
42

The way we have designed our classes, each Philosopher class will always have a method "the_answer". Let's assume, we don't know a priori if we want or need this method. Let's assume that the decision, if the classes have to be augmented, can only be made at runtime. This decision might depend on configuration files, user input or some calculations.

# the following variable would be set as the result of a runtime calculation:
reply = input("Do you need the answer? (y/n): ")
required = reply in ('y', 'yes')
   

def the_answer(self, *args):              
        return 42

    
class Philosopher1: 
    pass
if required:
    Philosopher1.the_answer = the_answer

    
class Philosopher2: 
    pass
if required:
    Philosopher2.the_answer = the_answer

    
class Philosopher3: 
    pass
if required:
    Philosopher3.the_answer = the_answer
    
    
plato = Philosopher1()
aristotle = Philosopher2()

# let's see what Plato and Aristotle have to say :-)
if required:
    print(aristotle.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")

OUTPUT:

42
42

Alternatively, we could have written this code in the following way. Yet, it doesn't make a lot of difference. We jusst unite three if statements in one:

def the_answer(self, *args):              
        return 42

class Philosopher1(object): 
    pass

class Philosopher2(object): 
    pass

class Philosopher3(object): 
    pass

if required:
    Philosopher1.the_answer = the_answer
    Philosopher2.the_answer = the_answer
    Philosopher3.the_answer = the_answer

Even though this is another solution to our problem, there are still some serious drawbacks. It's error-prone, because we have to add the same code to every class and it seems likely that we might forget it. Besides this it's getting hardly manageable and maybe even confusing, if there are many methods we want to add.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Solution with a Manager Function

We can improve our approach by defining a manager function and avoiding redundant code this way. The manager function will be used to augment the classes conditionally.

# the following variable would be set as the result of a runtime calculation:
reply = input("Do you need the answer? (y/n): ")
required = reply in ('y', 'yes')
    

def the_answer(self, *args):              
        return 42

    
# manager function
def augment_answer(cls):                      
    if required:
        cls.the_answer = the_answer
        
    
class Philosopher1: 
    pass
augment_answer(Philosopher1)


class Philosopher2: 
    pass
augment_answer(Philosopher2)


class Philosopher3: 
    pass
augment_answer(Philosopher3)
    
    
plato = Philosopher1()
aristotle = Philosopher2()


# let's see what Plato and aristotle have to say :-)
if required:
    print(aristotle.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")

OUTPUT:

42
42

Class Decorator

Our philosophers cam up with a better idea: Class Decoration

Philosophers brooding over Class decoration

This is better than the previous solution to solve our problem, but we, i.e. the class designers, must be careful not to forget to call the manager function "augment_answer". The code should be executed automatically. We need a way to make sure that "some" code might be executed automatically after the end of a class definition.

# the following variable would be set as the result of a runtime calculation:
reply = input("Do you need the answer? (y/n): ")

required = reply in ('y', 'yes')
    
    
def the_answer(self, *args):              
        return 42

    
def augment_answer(cls):                      
    if required:
        cls.the_answer = the_answer
    # we have to return the class now:
    return cls
 
    
@augment_answer
class Philosopher1: 
    pass


@augment_answer
class Philosopher2: 
    pass


@augment_answer
class Philosopher3: 
    pass
 
    
plato = Philosopher1()
aristotle = Philosopher2()
  
    
# let's see what Plato and aristotle have to say :-)
if required:
    print(aristotle.the_answer())
    print(plato.the_answer())
else:
    print("The silence of the philosphers")

OUTPUT:

42
42

This concept of class decoration is often used in Python for metaprogramming, where you modify classes or objects dynamically at runtime based on certain conditions or requirements. Class decorators are a powerful tool in Python for extending or modifying the behavior of classes and instances. They allow you to add functionalities, modify methods, or inject attributes dynamically.

Metaclasses can also be used for this purpose as we will learn in the next chapter.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here

A more Realistic Example

def add_class_variable(cls):
    cls.new_variable = "This is a new class variable added by the decorator"
    return cls

@add_class_variable
class MyClass:
    def __init__(self, value):
        self.value = value

# Now let's create an instance of MyClass and access the added class variable
obj = MyClass(10)
print(obj.new_variable)  # Output: This is a new class variable added by the decorator

OUTPUT:

This is a new class variable added by the decorator

Let's improve our example so that we can add the class variable as a parameter to our function:

def add_class_variable(variable_name, value):
    def decorator(cls):
        setattr(cls, variable_name, value )
        return cls
    return decorator

@add_class_variable('city', 'Erlangen')
class MyClass:
    def __init__(self, value):
        self.value = value

# Now let's create an instance of MyClass and access the added class variable
obj = MyClass(10)
print(obj.city)  # Output: This is a new class variable added by the decorator

OUTPUT:

Erlangen

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Upcoming online Courses

Enrol here