Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python How to force object instantiation via Context Manager?

I want to force object instantiation via class context manager. So make it impossible to instantiate directly.

I implemented this solution, but technically user can still instantiate object.

class HessioFile:
    """
    Represents a pyhessio file instance
    """
    def __init__(self, filename=None, from_context_manager=False):
        if not from_context_manager:
            raise HessioError('HessioFile can be only use with context manager')

And context manager:

@contextmanager
def open(filename):
    """
    ...
    """
    hessfile = HessioFile(filename, from_context_manager=True)

Any better solution ?

like image 923
Jean Jacquemier Avatar asked Oct 28 '16 09:10

Jean Jacquemier


People also ask

How is an object instantiated in Python?

In short, Python's instantiation process starts with a call to the class constructor, which triggers the instance creator, . __new__() , to create a new empty object. The process continues with the instance initializer, . __init__() , which takes the constructor's arguments to initialize the newly created object.

What actions are guaranteed by a context manager?

A context manager usually takes care of setting up some resource, e.g. opening a connection, and automatically handles the clean up when we are done with it. Probably, the most common use case is opening a file. The code above will open the file and will keep it open until we are out of the with statement.

What does __ exit __ do in Python?

__exit__() method The __exit__ method takes care of releasing the resources occupied with the current code snippet. This method must be executed no matter what after we are done with the resources.

What should __ exit __ return?

__enter__() is provided which returns self while object. __exit__() is an abstract method which by default returns None .


4 Answers

If you consider that your clients will follow basic python coding principles then you can guarantee that no method from your class will be called if you are not within the context.

Your client is not supposed to call __enter__ explicitly, therefore if __enter__ has been called you know your client used a with statement and is therefore inside context (__exit__ will be called).

You just need to have a boolean variable that helps you remember if you are inside or outside context.

class Obj:
    def __init__(self):
        self._inside_context = False

    def __enter__(self):
        self._inside_context = True
        print("Entering context.")
        return self

    def __exit__(self, *exc):
        print("Exiting context.")
        self._inside_context = False

    def some_stuff(self, name):
        if not self._inside_context:
            raise Exception("This method should be called from inside context.")
        print("Doing some stuff with", name)

    def some_other_stuff(self, name):
        if not self._inside_context:
            raise Exception("This method should be called from inside context.")
        print("Doing some other stuff with", name)


with Obj() as inst_a:
    inst_a.some_stuff("A")
    inst_a.some_other_stuff("A")

inst_b = Obj()
with inst_b:
    inst_b.some_stuff("B")
    inst_b.some_other_stuff("B")

inst_c = Obj()
try:
    inst_c.some_stuff("c")
except Exception:
    print("Instance C couldn't do stuff.")
try:
    inst_c.some_other_stuff("c")
except Exception:
    print("Instance C couldn't do some other stuff.")

This will print:

Entering context.
Doing some stuff with A
Doing some other stuff with A
Exiting context.
Entering context.
Doing some stuff with B
Doing some other stuff with B
Exiting context.
Instance C couldn't do stuff.
Instance C couldn't do some other stuff.

Since you'll probably have many methods that you want to "protect" from being called from outside context, then you can write a decorator to avoid repeating the same code to test for your boolean:

def raise_if_outside_context(method):
    def decorator(self, *args, **kwargs):
        if not self._inside_context:
            raise Exception("This method should be called from inside context.")
        return method(self, *args, **kwargs)
    return decorator

Then change your methods to:

@raise_if_outside_context
def some_other_stuff(self, name):
    print("Doing some other stuff with", name)
like image 154
cglacet Avatar answered Oct 11 '22 13:10

cglacet


None that I am aware of. Generally, if it exists in python, you can find a way to call it. A context manager is, in essence, a resource management scheme... if there is no use-case for your class outside of the manager, perhaps the context management could be integrated into the methods of the class? I would suggest checking out the atexit module from the standard library. It allows you to register cleanup functions much in the same way that a context manager handles cleanup, but you can bundle it into your class, such that each instantiation has a registered cleanup function. Might help.

It is worth noting that no amount of effort will prevent people from doing stupid things with your code. Your best bet is generally to make it as easy as possible for people to do smart things with your code.

like image 42
fspmarshall Avatar answered Oct 11 '22 15:10

fspmarshall


I suggest the following approach:

class MainClass:
    def __init__(self, *args, **kwargs):
        self._class = _MainClass(*args, **kwargs)

    def __enter__(self):
        print('entering...')
        return self._class

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Teardown code
        print('running exit code...')
        pass


# This class should not be instantiated directly!!
class _MainClass:
    def __init__(self, attribute1, attribute2):
        self.attribute1 = attribute1
        self.attribute2 = attribute2
        ...

    def method(self):
        # execute code
        if self.attribute1 == "error":
            raise Exception
        print(self.attribute1)
        print(self.attribute2)


with MainClass('attribute1', 'attribute2') as main_class:
    main_class.method()
print('---')
with MainClass('error', 'attribute2') as main_class:
    main_class.method()

This will outptut:

entering...
attribute1
attribute2
running exit code...
---
entering...
running exit code...
Traceback (most recent call last):
  File "scratch_6.py", line 34, in <module>
    main_class.method()
  File "scratch_6.py", line 25, in method
    raise Exception
Exception
like image 32
Laurent Chriqui Avatar answered Oct 11 '22 15:10

Laurent Chriqui


You can think of hacky ways to try and enforce this (like inspecting the call stack to forbid direct calls to your object, boolean attribute that is set upon __enter__ that you check before allowing other actions on the instance) but that will eventually become a mess to understand and explain to others.

Irregardless, you should also be certain that people will always find ways to bypass it if wanted. Python doesn't really tie your hands down, if you want to do something silly it lets you do it; responsible adults, right?

If you need an enforcement, you'd be better off supplying it as a documentation notice. That way if users opt to instantiate directly and trigger unwanted behavior, it's their fault for not following guidelines for your code.

like image 36
Dimitris Fasarakis Hilliard Avatar answered Oct 11 '22 14:10

Dimitris Fasarakis Hilliard