Friday 15 July 2022

Creating classes dynamically

Python classes are profoundly dynamic. We can add new methods to a class or an instance, modify the inheritance chain, switch one method implementation for another one, or dynamically create a new class... Remember from my previous post that classes are instances of the type metaclass or of a custom metaclass.

Given these methods and data attributes that we want in our dynamic class:


# initializer
def init(self, name):
    self.name = name
  
# method
def say_hi(self, to):
    return f"{self.name} says hi to {to}"
  
# class method
@classmethod
def my_class_method(cls, arg):
    return f"{cls.__name__}.{cls.my_class_method.__name__} invoked with arg: {arg}, {cls.class_attribute}"

class_items = {
    # constructor
    "__init__": init,
      
    # class data
    "class_attribute": "AAAA",
      
    # methods
    "say_hi": say_hi,
    "my_class_method": my_class_method
}

def test_dynamic_class(cls):
    p1 = cls("Francois")
    print(p1.say_hi("Antoine"))
    print(cls.my_class_method("BB"))
    print(cls.class_attribute)

We can create a class with type as its metaclass by invoking type like this:


# creating class dynamically
Person = type("Person", (object, ), class_items)

test_dynamic_class(Person)

# Francois says hi to Antoine
# Person.my_class_method invoked with arg: BB, AAAA
# AAAA


An alternative would be creating an "empty" class and adding the methods and data attributes like this:


class Person2:
    pass

for key, value in class_items.items():
    setattr(Person2, key, value)

test_dynamic_class(Person2)

# Francois says hi to Antoine
# Person2.my_class_method invoked with arg: BB, AAAA
# AAAA


If we want to dynamically create a class using a custom metaclass we have to use types.new_class. Notice that rather than passing a dictionary with the attributes, we pass a function that adds those attributes to the "class namespace"


class Meta1(type):
    pass

def fill_namespace(ns):
    ns.update(class_items)

Person3 = types.new_class(
    name="Person3",
    #bases=(B, C),
    kwds={"metaclass": Meta1},
    exec_body=fill_namespace
)

test_dynamic_class(Person3)

# Francois says hi to Antoine
# Person3.my_class_method invoked with arg: BB, AAAA
# AAAA

We can also use the alternative technique of creating an "empty class" (but with our custom metaclass): class Person3(metaclass=Meta1) and then adding the methods and data attributes to it.

No comments:

Post a Comment