Skip to content

Abstract Factory

This page presents the AbstractFactory metaclass which implement an abstract factory design pattern. The abstract factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes. This design pattern is also known as "Factory of Factories" because it is a "super-factory" which creates other factories. This metaclass proposes an implementation for creating a factory of related objects without explicitly specifying their classes. Each generated factory can instantiate the objects as per the Factory pattern.

Create a factory

The AbstractFactory metaclass is not a factory, but it contains the blueprint on how to build a factory. To create a factory, you will define a base class. This class is used to register the other classes but also to define the common interface of the factory. In the following of the documentation, we will call this class the base factory class. The example below shows the minimal implementation of a base class:

from objectory import AbstractFactory


class BaseClass(metaclass=AbstractFactory):
    pass

The base class should inherit the AbstractFactory metaclass. This metaclass will implement the factory mechanism to the base class. The base class can implement functions and attributes like a regular python class. It can also be an abstract class or not.

Register an object

This section explains the basics on how to register an object to a factory. It is possible to register a function. This section assumes you already created a factory as explained above. There are two approaches to register an object to the factory:

  • by using the inheritance (only for class)
  • by using the register_object function (both class and function)

Inheritance

The recommended approach to register a class to the factory is to use the inheritance. Every time that you create a new child class of the base factory class, the child class is automatically registered to the factory. For example, you can define the Child1Class class with the following implementation:

from objectory import AbstractFactory


class BaseClass(metaclass=AbstractFactory):
    pass


class Child1Class(BaseClass):
    pass

When the Child1Class class is created, it is automatically added to the BaseClass factory. The developer does not have to write any additional line to register the class. It is possible to define more complex child classes with a constructor or any functions/attributes. For example, you can define the Child2Class class with the following implementation:

class Child2Class(BaseClass):
    def __init__(self, dim: int):
        self.dim = dim

Similarly, any grand child class of the base factory class is automatically registered to the factory. In the following example, the Child3Class class which inherits from Child1Class class will be added to the BaseClass factory:

class Child3Class(Child1Class):
    pass

register_object function

The inheritance approach works when it is possible to modify the source code of the classes because each registered need to inherit from the base factory class. However, it is not possible to do that in particular when the code depends on a third party library. To overcome this limitation, it is possible to use the register_object function to manually register some classes that are defined outside the project.

Let's take the example of PyTorch. PyTorch is an open source machine learning framework. To define a machine learning model in PyTorch, you can implement a new class that inherits from the torch.nn.Module class. PyTorch also provides many module implementations. If you want to build a factory that includes both your module implementations and the PyTorch ones, you can do it by using the inheritance and the register_object function. First, you need to define the base factory class:

import torch
from objectory import AbstractFactory


class BaseModule(torch.nn.Module, metaclass=AbstractFactory):
    pass


class MyModule1(BaseModule):
    pass


class MyModule2(BaseModule):
    pass

Then you can register some modules implemented in PyTorch. For example if you want to register the class torch.nn.Linear to the factory of BaseModule, you can write the following lines:

import torch

BaseModule.register_object(torch.nn.Linear)

The register_object function can be used to register a class but also a function. To be consistent with the factory idea, you should only register functions that returns an object that is compatible with the common interface of the factory. Note that no warning will be raised if you do not follow this rule, but it will be your responsibility to manage this situation. Please keep in mind that with power comes responsibility. The following example shows how to register a function:

import torch


def my_nodule(input_size: int, output_size: int) -> torch.nn.Module:
    return torch.nn.Sequential(
        torch.nn.Linear(input_size, 32),
        torch.nn.ReLU(),
        torch.nn.Linear(32, output_size),
    )


BaseModule.register_object(my_nodule)

Another approach to register a function is to use the decorator register:

import torch
from objectory import register


@register(BaseModule)
def my_nodule(input_size: int, output_size: int) -> torch.nn.Module:
    return torch.nn.Sequential(
        torch.nn.Linear(input_size, 32),
        torch.nn.ReLU(),
        torch.nn.Linear(32, output_size),
    )

The argument of the decorator is the base factory class where the function will be registered.

⚠ It is not possible to register a lambda function. Please use a regular python function instead. The registry will raise the exception IncorrectObjectFactoryError if you try to register a lambda function.

Registered objects

Inheritors

Sometimes it is important to know what are the registered objects to the factory. You can see the list of the objects that are registered to the base class by using the attribute inheritors. This attribute contains the full name of the class as well as the class. If you print the value of inheritors,

print(BaseClass.inheritors)

Output:

{
  "my_package.base.BaseClass": <class "my_package.base.BaseClass">,
  "my_package.child1.Child1Class": <class "my_package.child1.Child1Class">,
  "my_package.child2.Child2Class": <class "my_package.child2.Child2Class">,
  "my_package.child3.Child3Class": <class "my_package.child3.Child3Class">
}

This example assumes that the BaseClass is written in the file my_package/base.py and Child<X>Class is written in the file my_package/child<X>.py.

Note that the base class and all its children classes have the attribute inheritors so you can check the registered objects with any of these classes:

print(Child1Class.inheritors)
print(Child2Class.inheritors)

Output:

{
  "my_package.base.BaseClass": <class "my_package.base.BaseClass">,
  "my_package.child1.Child1Class": <class "my_package.child1.Child1Class">,
  "my_package.child2.Child2Class": <class "my_package.child2.Child2Class">,
  "my_package.child3.Child3Class": <class "my_package.child3.Child3Class">
}
{
  "my_package.base.BaseClass": <class "my_package.base.BaseClass">,
  "my_package.child1.Child1Class": <class "my_package.child1.Child1Class">,
  "my_package.child2.Child2Class": <class "my_package.child2.Child2Class">,
  "my_package.child3.Child3Class": <class "my_package.child3.Child3Class">
}

The key of each object is the full name of the object. If you register two objects with the same full class name (package name + module name + class name), only the last object will be registered. It is the responsibility of the user to manage the object name to avoid duplicate.

If you have registered some functions, you should see them in the inheritors. For example if you have registered the function my_nodule in the BaseModule, you should see something like: 'my_package.nodule.my_nodule': <function my_nodule at 0x...>.

Missing objects

To be added to the factory, an object has to be loaded at least one time. The objects are automatically registered when they are loaded the first time. An object which is never loaded will never be registered. If you do not see an object in the list of inheritors, it is probably because it was not loaded. First, verify that the object inherits from the base factory class or the register function is called. Then, check if the python module with the object is loaded at least one time.

A solution to this problem is to write the child classes in the same python module that the base factory class. However, this solution is not always good or possible. When there is a lot of objects, it is usually better to write them in several python modules.

Let's assume the following situation where each class is written in a different python module. The package should have the following structure:

my_package/
    __init__.py
    base.py
    child1.py
    child2.py

A solution is to import the child classes in the __init__.py. The file __init__.py should have the following lines

from my_package import child1
from my_package import child2

Another solution is to use the import package tool.

Factory

This section explains how to instantiate dynamically a class registered in a AbstractFactory. One of the operation done by the AbstractFactory metaclass is to add the factory function to the base factory class and all its child classes. This function can instantiate any registered class given its configuration. The signature of thefactory function is:

def factory(cls, _target_: str, *args, _init_: str = "__init__", **kwargs): ...

where *args and **kwargs are the parameters of the object to instantiate. The input _target_ is used to define the name of the object to instantiate and _init_ indicates the function used to create the object. The following sections will explain the role of each parameter and how to use them.

Target object

One of the key features of the factory function is that you can specify the name of the object that you want to instantiate. The _target_ input is used to define the name of the object to instantiate. For example if you want to create an Child1Class object, you can write:

my_obj = BaseClass.factory("my_package.child1.Child1Class")

When you instantiate an object, you can also specify the arguments. For example if you want to create a Child2Class object with 10 layers, you can write:

my_obj = BaseClass.factory("my_package.child2.Child2Class", num_layers=10)

Sometimes it can be time consuming to write the full class name. If the class name is unique, you can instantiate the object by only specifying the class name. In the previous example, instead of the full class name (my_package.child1.Child1Class) you can specify only the class name (Child1Class):

my_obj = BaseClass.factory("Child1Class")

The second approach is easier to use, but it forces each class name to be unique. Under the hood, the factory uses the name resolution mechanism to find the path where the class is. If the class name is not unique, the name resolution mechanism will not be able to instantiate the object because of the ambiguity. If several classes have the same name, you need to specify the full name to break the ambiguity.

Let's imagine the case where there are two classes with the same name Linear.

import torch

# Register the class Linear from PyTorch
BaseModule.register_object(torch.nn.Linear)


# Register another class Linear
class Linear(BaseModule):
    pass

There is no problem to register to class with the same class name because their full class name are different (torch.nn.modules.linear.Linear vs __main__.Linear). Please read the name resolution mechanism documentation to learn why the real full name of torch.nn.linear.Linear is torch.nn.modules.linear.Linear. If you want to instantiate the Linear class from PyTorch, you will need to give the full class name:

my_obj = BaseModule.factory("torch.nn.Linear", in_features=8, out_features=6)

If you want to instantiate the local Linear class, you will need to give the full class name:

my_obj = BaseModule.factory("__main__.Linear", in_features=8, out_features=6)

If you only specify Linear, the factory does not know what class you want to instantiate and will raise an error.

my_obj = BaseModule.factory("Linear", in_features=8, out_features=6)
# Raise the error: factory.error.UnregisteredObjectFactoryError: Unable to create the object Linear.
# Registered objects of BaseModule are {'__main__.Linear', 'torch.nn.modules.linear.Linear'}

If several classes have the same name, the only solution is to specify the full class name. Similarly to class, it is possible to specify the name of the function to call. For example if you want to call the my_nodule function previously defined, you can write:

net = BaseModule.factory("my_package.nodule.my_nodule", input_size=4, output_size=12)

Finally, you can call the factory function with any class that inherits from the base factory class:

my_obj = BaseClass.factory("my_package.child1.Child1Class")
# or
my_obj = Child1Class.factory("my_package.child1.Child1Class")
# or
my_obj = Child2Class.factory("my_package.child1.Child1Class")

Initialization function

By default, the factory function calls the function __init__ of a class to create an object. You can also create an object by using a class method. You can use the keyword to _init_ to specify the function to use to create the object. The default value of this keyword is "__init__" so you do not need to change it if you call the default constructor. Note the initialization function input is only available for the classes. This input is ignored when you call a function to instantiate an object.

Let's define a new class that has a class method to create an object:

# file: my_package/child4.py
class Child4Class(BaseClass):
    def __init__(self, dim: int):
        self.dim = dim

    @classmethod
    def default(cls):
        return cls(dim=256)

Then, you can create an Child4Class object with the default method with the following command:

my_obj = BaseClass.factory(
    _target_="my_package.child4.Child4Class",
    _init_="default",
)

Instantiate an unregistered object

The AbstractFactory metaclass provides some functionalities to dynamically instantiate an unregistered object. This feature is enabled by the name resolution mechanism. It is useful to instantiate an object which is not defined in a third party package. For example if you want to load the class torch.nn.GRU, you can use the following line:

net = BaseClass.factory("torch.nn.GRU", input_size=4, hidden_size=12)

It is not necessary to register this class to instantiate it. If the _target_ value is a module path, the factory function tries to import it. If the import is successful, the object is registered in the factory, so it is possible to reuse it later. This functionality is also useful when an object can be initialized by specifying several ways. Due to some imports in the __init__.py of some packages, some objects have several module paths. For example, a GRU object in PyTorch can be created by using the two approaches below:

net = BaseClass.factory("torch.nn.GRU", input_size=4, hidden_size=12)
net = BaseClass.factory("torch.nn.modules.rnn.GRU", input_size=4, hidden_size=12)

Please read the name resolution mechanism documentation to learn more about it.

Tools

This section presents some useful tools for the AbstractFactory metaclass.

Register all the child classes

In some cases, you may want to register a class and all its child classes. Instead of doing manually, you can use the function register_child_classes. This function will automatically register the given class and all its child classes. It will find all the child classes and will add them to the registry. It finds the child classes recursively, so it will also find the child classes of the child classes.

Let's imagine you have the following classes, and you want to register them to the base class factory BaseClass:

# file: my_package/my_module.py
from objectory.abstract_factory import register_child_classes


class Foo:
    pass


class Bar(Foo):
    pass


class Baz(Foo):
    pass


class Bing(Bar):
    pass


register_child_classes(BaseClass, Foo)
print(BaseClass.inheritors)

Output:

{
  'my_package.my_module.Foo',
  'my_package.my_module.Baz',
  'my_package.my_module.Bing',
  'my_package.my_module.Bar'
}

By default, the function register_child_classes ignores all the abstract classes because you cannot instantiate them. If you want to also register the abstract classes, you can use the argument ignore_abstract_class=False. The following example shows how to register all the torch.nn.Module including the abstract classes.

import torch
from objectory.abstract_factory import register_child_classes

register_child_classes(BaseClass, torch.nn.Module, ignore_abstract_class=False)

Error messages

This section lists some of the most frequent error messages and explain how to fix the error. If you try to instantiate a class that is not registered (e.g. Child3Class), you will see the following error message:

objectory.abstract_factory.UnregisteredClassAbstractFactoryError: Unable to create
the class Child3Class. Verify that the class was added to the __init__.py file of its module.
Registered child classes of BaseClass are ["child1class", "child2class"]

The error shows the list of classes that are registered (["my_package.base.BaseClass", "my_package.child1.Child1Class", "my_package.child2.Child2Class"] in this example). If your class does not appear in that list, it is probably because your class was never imported and registered. Keep in mind that each class has to be loaded at least once to be registered. The error AbstractClassAbstractFactoryError will be raised if you try to instantiate an abstract class because an abstract class cannot be instantiated.

Limitations

The AbstractFactory metaclass adds some attributes and methods to the classes. To avoid potential conflicts with the other classes, all the non-public attributes and functions starts with _abstractfactory_**** where **** is the name of the attribute or the function. As presented above, you cannot define the following methods:

  • inheritors
  • factory
  • register_object
  • unregister