Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using context managers without "with" block

Below is an example of my my_create method, and an example of that method in use.

@contextmanager
def my_create(**attributes):
    obj = MyObject(**attributes)
    yield obj
    obj.save()

with my_create(a=10) as new_obj:
     new_obj.b = 7

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.is_saved()  # => True

To users of Ruby/Rails, this may look familiar. It's similar to the ActiveRecord::create method, with the code inside the with block acting as, well, a block.

However:

with my_create(a=10) as new_obj:
    pass

new_obj.a  # => 10
new_obj.is_saved()  # => True

In the above example, I've passed an empty "block" to my my_create function. Things work as expected (my_obj was initialized, and saved), but the formatting looks a little wonky, and the with block seems unnecessary.

I would prefer to be able to call my_create directly, without having to setup a passing with block. Unfortunately, that's not possible with my current implementation of my_create.

my_obj = create(a=10)
my_obj  # => <contextlib.GeneratorContextManager at 0x107c21050>

I'd have to call both __enter__ and __exit__ on the GeneratorContextManager to get my desired result.

The question:

Is there a way to write my my_create function so that it can be called with a "block" as an optional "parameter"? I don't want to pass an optional function to my_create. I want my_create to optionally yield execution to a block of code.

The solution doesn't have to involve with or contextmanager. For instance, the same results as above can be achieved with a generator and a for loop, although the syntax becomes even more unclear.

At this point I'm afraid that a readable-enough-to-be-sensibly-usable solution doesn't exist, but I'm still interested to see what everyone comes up with.

Some clarification:

Another example would be:

@contextmanager
def header_file(path):
    touch(path)
    f = open(path, 'w')
    f.write('This is the header')
    yield f
    f.close()

with header_file('some/path') as f:
    f.write('some more stuff')

another_f = header_file('some/other/path')

I always want to do the __enter__ and __exit__ parts of the context manager. I don't always want to supply a block. I don't want to have to set up a passing with block if I don't have to.

This is possible and easy in Ruby. It would be cool if it were possible in Python too, since we're already so close (we just have to set up a passing with block). I understand that the language mechanics make it a difficult (technically impossible?) but a close-enough solution is interesting to me.

like image 900
chmod 777 j Avatar asked Feb 02 '18 22:02

chmod 777 j


People also ask

Can context managers be used outside the with statement?

Yes, the context manager will be available outside the with statement and that is not implementation or version dependent. with statements do not create a new execution scope.

When would you use 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.

Which methods are required to implement context management protocol?

Context managers can be written using classes or functions(with decorators). Creating a Context Manager: When creating context managers using classes, user need to ensure that the class has the methods: __enter__() and __exit__().

What does the context manager do when you are opening a file using with in Python?

The with statement in Python is a quite useful tool for properly managing external resources in your programs. It allows you to take advantage of existing context managers to automatically handle the setup and teardown phases whenever you're dealing with external resources or with operations that require those phases.


Video Answer


1 Answers

Add a new method on MyObject which creates and saves.

class MyObject:

    @classmethod
    def create(cls, **attributes):
        obj = cls(**attributes)
        obj.save()
        return obj

This is an alternate initializer, a factory, and the design pattern has precedent in Python standard libraries and in many popular frameworks. Django models use this pattern where an alternate initializer Model.create(**args) can offer additional features that the usual Model(**args) would not (e.g. persisting to the database).

Is there a way to write my my_create function so that it can be called with a "block" as an optional "parameter"?

No.

like image 199
wim Avatar answered Oct 06 '22 09:10

wim