Search code examples
pythondictionaryorganization

Python: Move dictionary within a class - with class methods as values - to another file


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.


Solution

  • 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