2

The goal is to define and initialize a python class that acts as an interface to resources that involve blocking IO Operations during initialization, and normal program operation.

Based on a few other posts here, The best that I could come up with is as below, Is there a better way, and if not, why not?

class IOResourceInterface:
    def __init__(self, url, config={}):
        # Normal class initialization
        # store URL(s) to resources (such as files, or Network bound resources)
        # Store Configurations (like database creds, etc.)
        pass

    async def init_coro(self):
        # Eagerly Initializing Connection to External Resources
        await asyncio.sleep(1) # simulates the initialization of IO Resources
        
    
    def __await__(self) -> "IOResourceInterface":
        yield from self.init_coro().__await__()
        return self

    async def do_stuff(self):
        pass

# And then within the event loop
resource = await IOResourceInterface("protocol://resource",config={"user":"", "pass":""})

# Here resource is fully initialized and ready to go
await resource.do_stuff()
MKX
  • 80
  • 6
  • As an aside and it may not be an issue with your initialization code in method `__init__`, but to be sure that when the caller of `IOResourceInterface()` doesn't specify the *config* argument that you always start out with an empty `config` dictionary, you should code the argument as *config=None* and in the method itself add `if config is None: config = {}`. – Booboo Sep 20 '21 at 18:29
  • It's okay to have a shared configuration object across all instances if you know what you're doing. Also, this is irrelevant to the question at hand. – MKX Sep 21 '21 at 07:51
  • As I said, it may not be an issue with *your* code and it was mentioned as an aside. I just did not want it to be a source of a second problem. – Booboo Sep 21 '21 at 09:19

1 Answers1

1

Which approach to use always depends on purpose of the class and the surrounding code.

But I prefer two approaches:

  • Factory method. In the case, asynchronous class method carry out all the necessary initialization and pass the initialized objects to the __init__ method as dependency injection:
class IOResourceInterface:
    def __init__(self, initialized_resources):
        pass

    async def do_stuff(self):
        pass

    @classmethod
    async def create(cls, url, config):
        # Eagerly Initializing Connection to External Resources
        await asyncio.sleep(1)  # simulates the initialization of IO Resources
        initialized_resources = {}
        return cls(initialized_resources)


io = await IOResourceInterface.create("protocol://resource", config={})
await io.do_stuff()
  • Asynchronous context manager. If a class requires not only initialization, but also direct deinitialization (closing connections, cleaning up resources and etc.), it is often useful to make it as an asynchronous context manager using methods __aenter__ and __aexit__. All work with the class instance is in context manager block:
class IOResourceInterface:
    def __init__(self, url, config):
        pass

    async def __aenter__(self):
        await asyncio.sleep(1)  # simulates the initialization of IO Resources
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await asyncio.sleep(1)  # simulates cleaning resources

    async def do_stuff(self):
        pass

async with IOResourceInterface("protocol://resource", config={}) as io:
    await io.do_stuff()
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • 1
    Thanks, The Async ctx manager is a very interesting structure indeed. In fact, if the IO Resource is an async database connection with locking and transactions, a combination of a Factory method and context managers will be a very powerful tool. – MKX Sep 22 '21 at 07:56