Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrapping homogeneous Python objects

Tags:

python

I'm looking for a way to have a collection of homogeneous objects, wrap them in another object, but have the wrapper object have the same API as the original and forward the corresponding API call to its object members.

class OriginalApi:
  def __init__(self):
    self.a = 1
    self.b = "bee"

  def do_something(self, new_a, new_b, put_them_together=None):
    self.a = new_a or self.a
    self.b = new_b or self.b

    if put_them_together is not None:
      self.b = "{}{}".format(self.a, self.b)

  # etc.

class WrappedApi:
  def __init__(self):
    self.example_1 = OriginalApi()
    self.example_2 = OriginalApi()

Some possible solutions that have been considered, but are inadequate:

  • Rewriting the whole API Why not? Not adequate because the API is fairly large and expanding. Having to maintain the API in multiple spots is not realistic.

    Code example:

    class WrappedApi:
      def __init__(self):
        self.example_1 = OriginalApi()
        self.example_2 = OriginalApi()
    
      def do_something(self, new_a, new_b, put_them_together=None):
        self.example_1.do_something(new_a, new_b, put_them_together)
        self.example_2.do_something(new_a, new_b, put_them_together)
    
  • Using a list and a for-loop This changes the API on the object. That said, this is the backup solution in the event I can't find something more elegant. In this case, the WrappedApi class would not exist.

    Code example:

    wrapped_apis = [OriginalApi(), OriginalApi()]
    for wrapped_api in wrapped_apis:
      wrapped_api.do_something(1, 2, True)
    
  • I tried using Python Object Wrapper, but I could not see how to have it call multiple sub-objects with the same arguments.

And for anyone curious about the use case, it's actually a collection of several matplotlib axes objects. I don't want to reimplement to entire axes API (it's big), and I don't want to change all the code that makes calls on axes (like plot, step, etc.)

like image 748
TinyTheBrontosaurus Avatar asked Mar 17 '19 19:03

TinyTheBrontosaurus


2 Answers

If you're only implementing methods then a generic __getattr__ can do the trick

class Wrapper: 
    def __init__(self, x): 
        self.x = x 
    def __getattr__(self, name): 
        def f(*args, **kwargs): 
            for y in self.x: 
                getattr(y, name)(*args, **kwargs) 
        return f

For example with x = Wrapper([[], [], []]) after calling x.append(12) all the three list objects will have 12 as last element.

Note that the return value will always be None... an option could be collecting return values and returning them as a list but this of course would "break the API".

like image 163
6502 Avatar answered Nov 20 '22 17:11

6502


I think you have the right idea here

wrapped_apis = [OriginalApi(), OriginalApi()]
for wrapped_api in wrapped_apis:
    wrapped_api.do_something(1, 2, True)

You can define your wrapper class by inheriting from list and then handle the API calls to its items once it is created.

class WrapperClass(list):
    def __init__(self, api_type):
        self.api_type = api_type

        for func in dir(api_type):
            if callable(getattr(api_type, func)) and not func.startswith("__"):
                setattr(self, func, lambda *args, **kwargs: 
                    [getattr(o, func)(*args, **kwargs) for o in self])

w = WrapperClass(OriginalApi)
o1, o2 = [OriginalApi()]*2
w.append(o1)
w.append(o2)
print(w.do_something(1, 2, True))
# [None, None]
print(w[0].b)
# 12
print(w[1].b)
# 12
print(o1.b)
# 12

Here, I'm iterating every method in your API class and creating a method in the wrapper class that applies its arguments to all its list items. It then returns a list comprehension consisting of the results.

Needless to say, you should probably validate the type of a new object being appended to this WrapperClass like so,

def append(self, item):
    if not isinstance(item, self.api_type):
        raise TypeError('Wrong API type. Expected %s'.format(self.api_type))
    super(WrapperClass, self).append(item)
like image 35
darksky Avatar answered Nov 20 '22 16:11

darksky