21

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.

chmod 777 j
  • 547
  • 1
  • 5
  • 15
  • 1
    My inspiration for this question, if anyone's interested: http://wiki.c2.com/?BlocksInPython – chmod 777 j Feb 02 '18 at 23:25
  • You need to wrap yield obj with try finally for exception safety. – Neil G Feb 03 '18 at 22:11
  • not relying on `@contextmanager` and defining your own `__enter__` and `__exit__` alongside a method that could be used when the block isn't given might be an option. – minmaxavg Jun 03 '18 at 10:57

4 Answers4

7

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.

wim
  • 338,267
  • 99
  • 616
  • 750
  • I'm aware that `create` methods like this exist but it's not really answering my question. sorry if the question wasn't too clear. – chmod 777 j Feb 02 '18 at 22:53
  • 1
    OK, I've just edited to provide the answer to your literal question. – wim Feb 02 '18 at 22:55
  • Thanks wim, but you're answer is too short for community guidelines? – chmod 777 j Feb 02 '18 at 23:00
  • 4
    @j0eb: No no no no nopity nope nope no. Python is not Ruby. Passing blocks around is not a thing in Python, and `yield` means something completely different from in Ruby. No. – user2357112 Feb 02 '18 at 23:11
  • @j0eb You said "*the solution doesn't have to involve with or contextmanager*". I have offered you a Pythonic solution. Not sure what more you're asking for here. – wim Feb 02 '18 at 23:16
  • See here for extended discussion about the topic: http://wiki.c2.com/?BlocksInPython – chmod 777 j Feb 02 '18 at 23:24
  • @wim Feel free to refer to example solution if you'd like. – chmod 777 j Feb 02 '18 at 23:56
  • @user2357112 . Actually `yield` means nearly the same thing as it does in Ruby. During the execution of some code (A loop, A context setup/teardown) yield some object to some other block of code (A ruby block or the python block inside your for or with statement) and then potentially pick up the return value. Continue execution of the original code. – chmod 777 j Feb 03 '18 at 00:33
  • @j0eb: No, `yield` means something entirely different. In Ruby, `yield` means "call the block". In Python, `yield` means that the function is a generator function, and that the generator's stack frame should be suspended and removed from the stack, and `next` or `send` should return the argument given to `yield`. – user2357112 Feb 03 '18 at 00:35
  • A Python function with `yield` behaves fundamentally differently from a function without `yield`, even if the only `yield` is an `if False: yield` that never executes. Ruby's `yield` doesn't change a function so deeply. – user2357112 Feb 03 '18 at 00:40
  • @user2357112 I think you're oversimplifying the ruby side and overcomplicating the python side. Both yield statements yield some object to some other block of code before resuming execution. Thats their essential purpose. The mechanics of execution transfer may be different but its incorrect to call them "completely different". – chmod 777 j Feb 03 '18 at 00:42
  • True there is the generator/function dichotomy but we're not asking "Are python functions and python generators different?". Generators aren't that different from Ruby functions that yield. Its just that you pretty much need to put that generator in a `with` or `for` block to be usefull. Of course you could go calling `next` but that's a little odd and generally not used. – chmod 777 j Feb 03 '18 at 00:45
  • But it sounds like you do know a lot about the topic so I'd love to see an answer to the original question. – chmod 777 j Feb 03 '18 at 00:46
  • The essential difference is that ruby yield doesn't yield unless there's something to yield to which I guess is the whole point of this question – chmod 777 j Feb 03 '18 at 00:52
4

I'd suggest using different functions to get a context manager that saves an object on __exit__ and to get an automatically saved object. There's no easy way to have one function do both things. (There are no "blocks" that you can pass around, other than functions, which you say you don't want.)

For instance, you could create a second function that just creates and immediately saves an object without running any extra code to run in between:

def create_and_save(**args):
    obj = MyObject(**args)
    obj.save()
    return obj

So you could make it work with two functions. But a more Pythonic approach would probably be to get rid of the context manager function and make the MyObject class serve as its own context manager. You can give it very simple __enter__ and __exit__ methods:

def __enter__(self):
    return self

def __exit__(self, exception_type, exception_value, traceback):
    if exception_type is None:
        self.save()

Your first example would become:

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

You could also turn the create_and_save function I showed above into a classmethod:

@classmethod
def create_and_save(cls, **args):
    obj = cls(**args)
    obj.save()
    return obj

Your second example would then be:

new_obj = MyObject.create_and_save(a=10)

Both of those methods could be written in a base class and simply inherited by other classes, so don't think you'd need to rewrite them all the time.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Hey thanks for the answer. I've updated my question with "some clarification" because it seems this is more tailored to a single purpose "I want to be able to create and save an object problem". – chmod 777 j Feb 03 '18 at 00:06
  • 2
    Your `header_file` function is actually another example of putting the context manager in the wrong place. Files are already context managers (which close themselves in `__exit__`). So make `header_file` a normal function that returns an open file to which it has written a header line. Then you can either use it in a `with` statement (with the file being the context manager), or just call the function and call `close` on the file afterwards yourself. – Blckknght Feb 03 '18 at 00:14
  • Thanks for the response.But then I have to call `close` on the file afterwards myself. Now my file isn't being context managed, I'm manually managing context JUST because I didn't want to do any extra handling of the file, like say, writing `'some more stuff'` to it. – chmod 777 j Feb 03 '18 at 00:21
  • It's like, as soon as I don't want to do anything inside the `with` block, I have to manage all of my context all of a sudden, or put an ungoldy `with`-`pass` block in my code. – chmod 777 j Feb 03 '18 at 00:22
  • Or I have to break DRY and repeat all of my methods so that they are wrapped as ContextManagers, or they do the _exact same thing_ except they don't have that one little yield statement that makes them work as context managers – chmod 777 j Feb 03 '18 at 00:26
3

Ok, there seems to be some confusion so I've been forced to come up with an example solution. Here's the best I've been able to come up with so far.

class my_create(object):
    def __new__(cls, **attributes):
        with cls.block(**attributes) as obj:
            pass

        return obj

    @classmethod
    @contextmanager
    def block(cls, **attributes):
        obj = MyClass(**attributes)
        yield obj
        obj.save()

If we design my_create like above, we can use it normally without a block:

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

And we can call it slightly differently with a block.

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

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.saved  # => True

Calling my_create.block is kind of similar to calling Celery tasks Task.s, and users who don't want to call my_create with a block just call it normally, so I'll allow it.

However, this implementation of my_create looks wonky, so we can create a wrapper to make it more like the implementation of context_manager(my_create) in the question.

import types

# The abstract base class for a block accepting "function"
class BlockAcceptor(object):
    def __new__(cls, *args, **kwargs):
        with cls.block(*args, **kwargs) as yielded_value:
            pass

        return yielded_value

    @classmethod
    @contextmanager
    def block(cls, *args, **kwargs):
        raise NotImplementedError

# The wrapper
def block_acceptor(f):
    block_accepting_f = type(f.func_name, (BlockAcceptor,), {})
    f.func_name = 'block'
    block_accepting_f.block = types.MethodType(contextmanager(f), block_accepting_f)
    return block_accepting_f

Then my_create becomes:

@block_acceptor
def my_create(cls, **attributes):
    obj = MyClass(**attributes)
    yield obj
    obj.save()

In use:

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

new_obj.a  # => 10
new_obj.b  # => 7
new_obj.saved  # => True


# creating without a block
new_obj = my_create(a=10)
new_obj.a  # => 10
new_obj.saved  # => True

Ideally the my_create function wouldn't need to accept a cls, and the block_acceptor wrapper would handle that, but I haven't got time to make those changes just now.

pythonic? no. useful? possibly?

I'm still interested to see what others come up with.

chmod 777 j
  • 547
  • 1
  • 5
  • 15
0

With a slight change, you can can really close to what you want, just not via implementation using contextlib.contextmanager:

creator = build_creator_obj()

# "with" contextmanager interface
with creator as obj:
     obj.attr = 'value'

# "call" interface
obj = creator(attr='value')

Where creator is an object that implements __enter__ and __exit__ for the first usage and implements __call__ for the second usage.

You can also hide the construction of creator inside a property on some persistent object, e.g.:

class MyDatabase():
    @property
    def create(self):
        return build_creator_obj()

db = MyDatabase()

# so that you can do either/both:
with db.create as obj:
  obj.attr = 'value'

obj = db.create(attr='value')
Kache
  • 15,647
  • 12
  • 51
  • 79