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

Time:04-02

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

CodePudding user response:

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)

CodePudding user response:

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]
            self.__dict__.update(attr.__dict__)

...
    

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

  • Related