I have a class that handles a TCP connection, and when receiving a message with a given "ID", I need to call a specific function to handle it. These IDs are just numbers, so I've created an IntEnum to hold the IDs:
class ID(IntEnum):
# ...
message_x = 20
message_y = 21
# ...
These IDs are not necessary contiguous (i.e. some IDs are reserved), and I expect to end up having hundreds or even thousands of IDs.
Because I don't want to create a thousand if - elses for each and every single ID, I was thinking of using the IDs as keys in a dictionary containing references to the functions that handle each message:
class ComManager:
def __init__(self):
# Init socket, message queues, threads for sending/receiving etc...
self.rcv_functions = {#...
ID.message_x: ComManager._rcv_message_x,
ID.message_y: ComManager._rcv_message_y,
#...
}
# Launch _rcv_thread here
def _rcv_thread(self):
message_id = self.rcv_message_id() # receive message ID from socket
message_id = ID(message_id) # Change from type "int" to type "ID"
self._rcv_functions[message_id](self) # Call appropriate method according to the dictionary, thus avoiding a massive if/else here or "switch case"-like workarounds
def _rcv_message_x(self):
# Handle reception and processing of message x here
def _rcv_message_y(self):
# Handle reception and processing of message y here
I've been trying to place "_rcv_functions" into its own file as it's already annoying enough to have a function per message:
# import ID and ComManager classes from their respetive files
_rcv_functions = {
# ...
ID.message_x: ComManager._rcv_message_x,
ID.message_y: ComManager._rcv_message_y,
# ...
}
Then, in ComManager:
class ComManager:
def __init__(self):
# Init socket, message queues, threads for sending/receiving etc...
from x import _rcv_functions
This obviously results in a circular dependency.
I've been searching for a solution to this problem and some people have suggested type hinting, but I haven't been able to get it working in this case.
I've also seen some answers suggesting the use of something like __import__('module_name').ComManager.class_method
for each dictionary value but I've read that this would heavily impact performance as the whole file would be processed each time I called __import__
, which is far from ideal since the dictionary will contain hundreds of entries.
Have you even tried it?
If you place the import
statement inside the __init__
method as you show above, there will be no "circular dependency": at the point the other module is imported by the first time, the caller module, where ComManager is defined had already run, and the class is defined and ready to be imported in the second module.
Other than that, you can just put the handling methods in a mixin class, instead of in the body of the handler ComManager
itself.
So, in the other module, you'd have:
...
class ID(IntEnum):
...
class HandlersMixin:
def _rcv_message_x(self, msg):
...
...
mapping = {
ID.message_x = HandlerMixn._rcv_message_x,
}
Note that in this way, the mapping maps the unbound methods: they are plain functions that expect an instance of "HandlerMixin" as their first parameter
And on your first module:
from other_module import ID, mapping, HandlerMixin
class ComManager(HandlerMixin):
def _rcv_thread(self):
message_id = self.rcv_message_id() # receive message ID from socket
message_id = ID(message_id) # Change from type "int" to type "ID"
mapping[message_id](self)
# Passing "self" explictly will make the methods work the same
# as f they were called from this instance as `self.method_name`,
# and since they are methods on this class through inheritance
# they can use any other methods or attributes on this instance