diff --git a/dev b/dev new file mode 120000 index 0000000..88d050b --- /dev/null +++ b/dev @@ -0,0 +1 @@ +main \ No newline at end of file diff --git a/main/404.html b/main/404.html new file mode 100644 index 0000000..dca69aa --- /dev/null +++ b/main/404.html @@ -0,0 +1,715 @@ + + + +
+ + + + + + + + + + + + + + +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.
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.
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:
+register_object
function (both class and function)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.
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...>
.
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.
+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.
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")
+
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",
+)
+
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.
+This section presents some useful tools for the AbstractFactory
metaclass.
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)
+
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.
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