Background:
The project involves creating a "Pipeline" which takes in various objects whose methods and properties are used to drive an analysis. Those objects should inherit from an abstract class defining abstract methods and properties. The intention from this is that the steps in the pipeline may be altered by new developers by overriding an existing class' methods whilst not needing to entirely understand the entire inner workings of the pipeline. Different pipelines may also be constructed using these classes in the future.
Problem:
Because different steps of this pipeline may require different input arguments (e.g. if one wants to change the definition of an evaluation, that evaluation may want additional arguments and may want those at runtime as opposed to being hardcoded into the class). An ideal situation would be that a programmer may inherit from an abstract class, redefine the signature to contain additional arguments, which are injected at runtime via a config, whilst the Pipeline still calls these methods with the original signatures. This would effectively give a highly flexible framework to build on as a developer as you can write whatever signatures you want as long as you define a config when running the script to handle them, whilst also meaning the pipeline never needs to be rewritten.
My solution:
My approach has been to basically build a class factory which builds classes at runtime. The idea is to take the class defined by the user, rip it apart and get all its data, replace every method with a partialmethod and return a new class from this new information.
Code:
class PartialClassFactory:
""" Acts as a decorator so that the user can define what needs to be partialled at runtime."""
def __init__(self, *method_attributes):
self.method_attributes = method_attributes
def __call__(self, cls):
cls._update_attrs = self.method_attributes
return cls
def get_partial_cls(self, cls, config):
partial_cls_attrs = dict(cls.__dict__)
partial_methods = {}
for method, attributes in cls._update_attributes:
new_values = {
attribute: config[attribute]
for attribute in attributes
if attribute in config.keys()
}
partial_methods[method] = partialmethod(cls.__dict__[method], **new_values)
partial_cls_attrs = partial_class_attrs.update(partial_methods)
partial_class_attrs.pop('__dict__')
partial_class_attrs.pop('__weakref__')
metaclass = type(cls)
return metaclass(f"Configured{cls.__name__}", cls.__bases__, partial_class_attrs)
return cls
@PartialClassFactory(('__init__', ('attr_to_replace')), ('method_a': ('attr_to_replace_')))
class ToBePartialled:
"""Not a real class, just demonstrating how the decorator would be used"""
def __init__(self, attr_to_replace):
self.attr_to_replace = attr_to_replace
def method_a(attr_to_replace_):
return attr_to_replace_
# Creating the new class would go something like
config = {'attr_to_replace': 1, 'attr_to_replace_': 2}
cls_factory = PartialClassFactory()
ConfiguredToBePartialled = cls_factory.get_partial_cls(ToBePartialled, config)
Questions: - This feels hacky, is there a better solution to the problem that I've missed? - Could there be issues with this solution? Is there any data from the original class that won't be retained that should be? - Are there any cleaner ways to implement the above solution?