'How do I compose different objects from a package/framework into a class or object the pythonic way?

Lets say I a have two objects of different framework that I cant alter (one is C implemented and the other uses a nested factory to instantiate):

# framework_a.py

class Foo:
    """
    produced by C implementation
    """
    ...
    text = "some dynamic text"
    
    # more methods to work with text
    ...
    
def provide_text():
    pass
        
# framework_b.py
    
class Bar:
    ...
    parsed_text = ["some", "dynamic", "text"]

# nested utility function 
def parse_from_plain(text):
    return text.split(" ")

So at runtime I'm getting two objects. Now for easier handling I want them to glue together. My solution is simple:

# userpackage.py
from framework_a import Foo
from framework_b import Bar, parse_from_plain

class Composed:
    def __init__(self, text, parsed_text):
        self.text = text
        self.parsed_text = parsed_text

# static function to run it in a Pool
def make_composed_object(foo_text: Foo, bar_parsed: Bar):
    ...
    # do some additional stuff before finally returning the object
    return Composed(foo_text, bar_parsed)
    
# main.py
from framework_a import provide_text
from userpackage import make_composed_object

...
db = []
for n in range(100_000_000)
    text = provide_text()
    parsed_text = parse_from_plain(text)
    obj = make_composed_object(text, parsed_text)
    
    obj.text.do_stuff()
    obj.parsed_text.do_even_more()

    # many more method calls depending on what user wants to do.

    db.append(obj)

...
    

however, this is not very nice. I like to have an interface like this

...
obj.do_stuff()
obj.do_even_more()

Other solutions i could imagine:

  • Monkeypath class Foo or Bar?
  • Metaprogramming?
  • Delegating many methods?
  • Descriptor?
  • make a script which parses the class Composed from the others class as abstract class. at init update the composed.dict


Solution 1:[1]

Given your example, Composed seems to be better as a dataclass.

from dataclasses import dataclass


@dataclass
class Composed:
    text: Foo
    parsed: Bar
    
    def __post_init__(self):
        """
        After initialising do some additional stuff
        replacing make_composed_object
        """
        self.text = NotImplemented
        self.parsed = NotImplemented
    
    def do_text_stuff(self):
        # does something that affects text state
        NotImplemented
    
    def do_parsed_stuff(self):
        # does something that affects parsed state
        NotImplemented

results:

def db_iter(num):
    for n in range(num):
        text = provide_text()
        parsed_text = parse_from_plain(text)
        composed = Composed(text, parsed_text)

        composed.do_text_stuff()
        composed.do_parsed_stuff()

        yield composed


db = db_iter(1_000_000)  # returns a Generator

NOTE: Keep in mind that you can actually call those two methods in __post_init__:

@dataclass
class Composed:
    text: Foo
    parsed: Bar
    
    def __post_init__(self):
        self._additional_stuff()
        self._text_stuff()
        self._parsed_stuff()
        
    def _additional_stuff(self):
        # additional stuff
        self.text = NotImplemented
        self.parsed = NotImplemented
    
    def _text_stuff(self):
        # does something that affects text state
        NotImplemented
    
    def _parsed_stuff(self):
        # does something that affects parsed state
        NotImplemented


def db_iter(num):
    for n in range(num)
        text = provide_text()
        parsed_text = parse_from_plain(text)
        yield Composed(text, parsed_text)

    
db = db_iter(1_000_000)

Solution 2:[2]

After playing around with ast and libCST I realized that there is a super simple solution

# userpackage.py
from framework_a import Foo
from framework_b import Bar, parse_from_plain

class Composed(Foo, Bar):
    def __init__(self, text, parsed_text):
        super().__init__()
        for param in [text, parsed_text]
            self.__dict__.update(attr.__dict__)

# or using a factory avoiding the init call
...
    

this will give the desired interface. However, name conflicts needs to be solved manually, but for my concrete problem this will work.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2