pythondesign-patternssingletongrpcgrpc-python

Python - Nested Singleton Classes


Is it possible to nest an arbitrary number Singleton classes within a Singleton class in Python?

There's no problem in changing my approach to solving this issue if a simpler alternative exists. I am just using the "tools in my toolset", if you will. I'm simulating some larger processes so bear with me if it seems a bit far-fetched.

An arbitrary number of gRPC servers can be started up and each server will be listening on a different port. So for a client to communicate with these servers, separate channels and thus separate stubs will need to be created for a client to communicate to a given server.

You could just create a new channel and a new stub every time a client needs to make a request to a server, but I am trying to incorporate some best practices and reuse the channels and stubs. My idea is to create a Singleton class that is comprised of Singleton subclasses that house a channel and stub pair as instance attributes. I would have to build the enclosing class in a way that allows me to add additional subclasses whenever needed, but I have never seen this type of thing done before.

The advantage to this approach is that any module that instantiates the main Singleton class will have access to the existing state of the channel and stub pairs, without having to recreate anything.

I should note that I already initialize channels and stubs from within Singleton classes and can reuse them with no problem. But the main goal here is to create a data structure that allows me to reuse/share a variable amount of gRPC channel and stub pairs.

The following is code for reusing the gRPC Channel object; the stubs are built in a very similar way, only difference is they accept the channel as an argument.

class gRPCChannel(object):
    _instance, _channel = None, None
    port: int = 50051

    def __new__(cls):
        """Subsequent calls to instance() return the singleton without repeating the initialization step"""
        if cls._instance is None:
            cls._instance = super(gRPCChannel, cls).__new__(cls)
            # The following is any initialization that needs to happen for the channel object
            cls._channel = grpc.insecure_channel(f'localhost:{cls.port}', options=(('grpc.enable_http_proxy', 0),))
        return cls._channel

    def __exit__(self, cls):
        cls._channel.close()

Solution

  • I think this is simpler than the "singleton pattern":

    # grpc.py
    import functools as ft
    
    class GrpcChannel:
        pass  # normal class, no funny __new__ overload business
    
    
    @ft.lru_cache
    def channel():
        return GrpcChannel()
    

    Usage:

    import grpc
    
    channel = grpc.channel()
    assert grpc.channel() is channel
    

    If you really want it all namespaced under the class (IMO no reason to, takes more typing & syntax for no extra benefit), then:

    class GrpcChannel:
        @classmethod
        @ft.lru_cache
        def instance(cls):
            return cls()
    
    # usage
    assert grpc.GrpcChannel.instance() is grpc.GrpcChannel.instance()