Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Extend Class / Object returned from a third party package that uses factory method

I'm trying to determine the best approach to wrap a third party class in another class, so I can provide a consistent internal API. The third party class is created using a fairly complicated factory method, and so is never instantiated directly. The other classes being wrapped are created directly.

Under normal instances I would simply inherit from the Class, and then extend as needed ie.

class MyCustomClassWithStandardAPI(StandardThirdPartyClass):
    def custom_method_that_defines_api(self):
        return super().third_party_method_being_wrapped()

But in this instance, because the object needs to be initialised via the third party factory method, I am unable to wrap the class, short of duplicating the factory method itself (which is long and so would be a risk if the factory changes in the future).

class MyCustomClassWithStandardAPI(ThirdPartyClassThatIsCreatedByFactoryMethod):
    def custom_method_that_defines_api(self):
        ....

def complicated_third_party_factory_method():
    ...
    ...
    returns ThirdPartyClassThatIsCreatedByFactoryMethod(a, b, c, d, ...)

Because the Class is created by the factory method, I am never able to interrupt the instantiation of the third party class and replace with my own subclass.

Any thoughts on how to achieve this in Python?

like image 356
Steven Avatar asked Oct 15 '25 04:10

Steven


2 Answers

If the other class goes out of normal collaborative inheritance for its instances creation process, and even for picking the classes which to instantiate, then your best approach there will certainly not be inheritance.

You will be better creating an association with one instance of the object in that other hierarchy, and keep yourself a clean and uniform interface acording to your needs.

That is:


class MyCustomBase:
    def __init__(self, [enough parameters to instantiate the 3rdy party thing you need]):
          self.api = third_party_factory(parameters)
          ...
    ...
    def stantard_comunicate(self, data):
         if isinstance(self, api, TypeOne):
               data = transform_data_for_type_one(data)
               self.api.comunicate_one(data)    
         elif isinstance(self, api, TypeTwo)
               data = transform_data_for_type_two(data)
               self.api.comunicate_two(data) 
         ...

If the 3rdy party lib has lots of methods and attributes, most of which you don't need to customize, you don't need to write then down one by one, either - you can customize attribute access on your wrapper class to defer to the wrapped attribute/method directly with a simple __getattr__ method. Inside your class above add:

    def __getattr__(self, attr):
        return getattr(self.api, attr)

Without any inheritance.

If you need inheritance...

Sometimes you will need your proxy object to "be" of the other kind, such as when passing your own instances to methods or classes provided by the other library. You can then force dynamic inheritance without meddling with the other side's factory method by dynamically creating a class with a much simpler factory method on your side:


def myfactory(...):

     instance = thirdy_party_factory(...)

     class MyClass(type(instance)):
         def __init__(self, ...):
             super().__init__(...)
             self.my_init_code()

         def my_init_code(self):
             # whatver you need to initialize the attributes
             #   you use go here
         ...
         # other methods you may want to customize can make use
         # of "super()" normally

     # Convert "other_instance" to be of type "my_instance":
     instance.__class__ = MyClass
     return instance

(If many such instances will be created in a long-lived process, then you should cache the dynamically created classes - The simpler way is just defer the class factory to an even simpler factory method that takes the parent class as parameter, and use Python's built-in "lru_cache" for that):

from functools import lru_cache


@lru_cache()
der inner_factory(Base):

     class MyClass(type(instance)):
         def __init__(self, ...):
             super().__init__(...)
             self.my_init_code()

         def my_init_code(self):
             # whatver you need to initialize the attributes
             #   you use go here
         ...
         # other methods you may want to customize can make use
         # of "super()" normally
    return MyClass



def myfactory(...):

     instance = thirdy_party_factory(...)
     MyClass = inner_factory(type(instance))
     # Convert "other_instance" to be of type "my_instance":
     instance.__class__ = MyClass
     return instance

Monkey Patching

The examples above are both on "your side only" of the story - but depending on how you are using things and on the nature of the other library, it may be easier to "monkey patch" it - that is: to replace, in the namespace of the 3rdy party factory method, the base classes it intends to use by your derived classes -so that your derieved classes will be used by the library's factory.

Monkey patching in Python is just a matter of assigning objects - but the standard library have a unittest.mock.patch callable that offers an utility that not only performs the patching, but takes care of the clean-up, restoring the original values, after the patched use is over.

If your library's factory do not use any Base classes itself, and builds all of the class inside the factory body, this approach can't be used. (and keep in mind that as this is not a proper "collaborative" practice, that library's authors are free to change this part of the design in any release).

But essentially:



from unittest.mock import patch
from thirdparty.bases import ThirdyBase
from thirdyparty.factories import factory

class MyStandardBase(ThirdyBase):
    def implement_standard_api(self, ...):
        ...


def mycode():
    # Bellow, the last item on the dotted path is the name
    # of the base class _inside_ the module that defines the
    # factory function.
    with patch("thirdyparty.factories.ThirdyBase", MyStandardBase):
        myinstance = factory(...)

like image 60
jsbueno Avatar answered Oct 17 '25 18:10

jsbueno


I would use duck typing here, because Python makes it easy. The hidden principle is that your own class just contains a subobject of the required and complex class, and transparently passes all the requests it does not know to that object.

The only problem, is that some code uses isinstance or issubclass, it will break.

If this is not a requirement, getattr can be enough:

class MyCustomClassWithStandardAPI:

    def __init__(self, thirdParty):             # store the "inherited" object at initialization time
        self.inner = thirdParty

    # define additional methods
    def custom_method_that_defines_api(self):
        ....

    # delegate everything else to the "inherited" object
    def __getattr__(self, attr):
        return getattr(self.inner, attr)

You use it that way:

obj = MyCustomClassWithStandardAPI(complicated_third_party_factory_method())
obj.custom_method_that_defines_api()     # Ok, uses the specialized method
obj.third_party_method()                 # Ok delegates to the inner object
like image 40
Serge Ballesta Avatar answered Oct 17 '25 18:10

Serge Ballesta