2

I'm interested in making my current Python project conform to the object-oriented paradigm as it was originally conceived by Alan Kay--which, from my understanding, is about objects messaging each-other as much as it's about the objects themselves.

Example code from the sort of package that I'm thinking of may look something like this:

def do_laundry(self):
    send(self.car, "DRIVE_TO", (self.laundromat))
    washing_machine = self.look_for(WashingMachine)
    send(self.washing_machine, "LOAD")
    ...

I can see how sticking closely to this paradigm could simplify object interfaces, and the basics of message-passing don't seem that difficult to implement, so I'm surprised that I haven't been able to find anything relevant. (All message-related packages that I've found relate to email or other Internet activities.) Is there a package that does what I'm looking for, or am I mistaken in believing that would be a useful thing for Python to have? It's conceivable that is hasn't been done and is potentially useful, but my past experiences indicate that this is unlikely.

P.S. I'm aware of this question, but it has a different focus, it hasn't been active for 10 years, and the link to the Python-specific resource which it refers to is broken.

1
  • Please review what is on-topic in the help center. Questions asking us to find or recommend an off-site resource, like a library, are explicitly off-topic as they tend to attract opinionated answers and spam. Commented Oct 24, 2021 at 17:55

2 Answers 2

3

You could use a pub sub system for this type of work. I'm a big fan of tools like rxpy, but the core concept of Pub-Sub is simple to add yourself. Take this for example:

class DriveTo:
    def drive_to(self, location):
        print(f"Driving To {location}")


class LoadWashingMachine:
    def load_washing_machine(self):
        print(f"Loading washing machine")


class DoLaundry:
    def __init__(self, laundromat):
        self._laundromat = laundromat
        self._drive_to_events = []
        self._load_washing_machine_events = []

    def _drive_to(self, location):
        for drive_event in self._drive_to_events:
            drive_event.drive_to(location)

    def _load_washing_machine(self):
        for washing_machine_event in self._load_washing_machine_events:
            washing_machine_event.load_washing_machine()

    def do_laundry(self):
        self._drive_to(self._laundromat)
        self._load_washing_machine()

Here we see that each component can be connected without needing to know much about the receiver of their messages. In the main of your application you would handle linking up each of the objects to the appropriate event methods. An example putting this together:

drive_to = DriveTo()
load_washing_machine= LoadWashingMachine()
do_laundry = DoLaundry(laundromat="cleaners")

do_laundry.drive_to_events.append(drive_to)
do_laundry.load_washing_machine_events.append(load_washing_machine)

do_laundry.do_laundry()

Going a step further we can see that the logic for each event is redundant. We can simplify this by make a generic class for events. Each type of message can be a different observable instance, and receivers can specify which types of events they subscribe to.

class Observer(ABC):
    @abstractmethod
    def on_next(self, *args, **kwargs):
        pass


class Observable:
    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def on_next(self, *args, **kwargs) -> None:
        for observer in self._observers:
            observer.on_next(*args, **kwargs)

Refactoring our initial example:

class DriveTo(Observer):
    def on_next(self, location):
        print(f"Driving To {location}")


class LoadWashingMachine(Observer):
    def on_next(self):
        print(f"Loading washing machine")


class DoLaundry:
    def __init__(self, laundromat):
        self._laundromat = laundromat
        self.drive_observable = Observable()
        self.load_observable = Observable()

    def do_laundry(self):
        self.drive_observable.on_next(self._laundromat)
        self.load_observable.on_next()


do_laundry = DoLaundry(laundromat="cleaners")
do_laundry.drive_observable.subscribe(DriveTo())
do_laundry.load_observable.subscribe(LoadWashingMachine())

do_laundry.do_laundry()

The rxpy library offers a LOT of utilities like this, which allow you to compose code very easily. It can handle things like filtering streams, combining them, handling exceptions, unsubscribing to events, and more.

Sign up to request clarification or add additional context in comments.

Comments

2

Another option to make objects easier to compose is to use a Dependency Injection (DI) framework or Inversion of Control (IoC) framework such as pinject.

In these systems we can define dependencies as normal constructor parameters, but allow the IoC system to handle hooking them together. Start with making you classes as you normally would:

class Car:
    def drive_to(self, location):
        print(f"Driving To {location}")


class WashingMachine:
    def load(self):
        print(f"Loading washing machine")


class DoLaundry:
    def __init__(self, laundromat, car, washing_machine):
        self._laundromat = laundromat
        self._car = car
        self._washing_machine = washing_machine

    def do_laundry(self):
        self._car.drive_to(self._laundromat)
        self._washing_machine.load()

Now we can allow pinject to hook everything together. For things like consts you can use a binding spec to help pinject determine what the values should be:

import pinject


class BindingSpec(pinject.BindingSpec):
    def provide_laundromat(self):
        return "cleaners"


obj_graph = pinject.new_object_graph(binding_specs=[BindingSpec()])
do_laundry = obj_graph.provide(DoLaundry)
do_laundry.do_laundry()

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.