'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 |