pass argument to __enter__
Asked Answered
L

7

62

Just learning about with statements especially from this article

question is, can I pass an argument to __enter__?

I have code like this:

class clippy_runner:
    def __enter__(self):
        self.engine = ExcelConnection(filename = "clippytest\Test.xlsx")
        self.db = SQLConnection(param_dict = DATASOURCES[STAGE_RELATIONAL])

        self.engine.connect()
        self.db.connect()

        return self

I'd like to pass filename and param_dict as parameters to __enter__. Is that possible?

Lepley answered 24/2, 2011 at 19:34 Comment(0)
C
61

Yes, you can get the effect by adding a little more code.


    #!/usr/bin/env python

    class Clippy_Runner( dict ):
        def __init__( self ):
            pass
        def __call__( self, **kwargs ):
            self.update( kwargs )
            return self
        def __enter__( self ):
            return self
        def __exit__( self, exc_type, exc_val, exc_tb ):
            self.clear()

    clippy_runner = Clippy_Runner()

    print clippy_runner.get('verbose')     # Outputs None
    with clippy_runner(verbose=True):
        print clippy_runner.get('verbose') # Outputs True
    print clippy_runner.get('verbose')     # Outputs None
Clamant answered 20/4, 2012 at 20:12 Comment(2)
This seems like the correct answer to my, as you don't need to create the variable in the with statement, but can use a readily created object (such as a lock) and pass variables to the with statement. Great answer!Katheryn
Excellent answer! This is should be the accepted as it can be used in a for loop for example, without instantiating a new object every iterationClaribel
A
51

No. You can't. You pass arguments to __init__().

class ClippyRunner:
    def __init__(self, *args):
        # save args as attributes 
        self._args = args
    
    def __enter__(self):
        # Do something with args
        print(self._args)


with ClippyRunner(args) as something:
    # work with "something"
    pass
Anatola answered 24/2, 2011 at 19:41 Comment(3)
I'm confused. Because you just pass in __init__ are you suggesting that args passed to __init__ are available in the __enter__ function?Newsman
Hovis: the args passed to init can be saved and then used in the enter method. def __init__(self, filename, param_dict): self.filename=filename self.param_dict=param_dict def __enter__(self): self.filename ...Prosecutor
Why is this the accepted answer? I seem to not be the only one who thinks that it is the worst answer of them all. "No you can't, you pass arguments to init.." is just simply not true, sure passing arguments through init is one method of doing it, but is by no means the only way, and is also not always the way that makes most sense.. If you want to pass args to the context manager at the with .. as .. line like the OP wanted to then you definitely can and this is often what makes most sense, depending on the application. (See all the other answers)Colostomy
C
25

The accepted answer (Which I feel is incorrect) states that you CAN'T, and that you should instead do;

class Comedian:
    def __init__(self, *jokes):
        self.jokes = jokes
    def __enter__(self):
        jokes = self.jokes
        #say some funny jokes
        return self

..and while this is often what you would do, it is not always the best solution, or even a solution, and it is definitely not the only solution!..

I assume that what you want is to be able to do something similar to;

funny_object = Comedian()
with funny_object('this is a joke') as humor:
    humor.say_something_funny()

If this is the case, and it is not more complicated than that, then you can just do;

class Comedian:
    def __enter__(self):
        jokes = self.jokes
        #say some funny jokes
        return self
    def __call__(self, *jokes):
        self.jokes = jokes
        return self  # EDIT as pointed out by @MarkLoyman

..That way you can still initialize the object with any arguments that you want, and do any other stuff with the object like you would usually, but when you go to use the object as a context manager you first call its call function and set up some args for the context manager.

The important thing here is to understand exactly how context managers work in Python.

In Python a context manager is any object that defines an enter method. This method is called automatically when you do;

with object as alias:
    alias.do_stuff()
    ..

..Notice how object doesn't have a couple of "()" after it, it is an implicit function call, and it doesn't take any arguments.

You might have gotten the idea of passing arguments to enter from;

with open(filename) as file:
    "do stuff with file..

But this is different from overriding enter, as "open" is not an object, but a function.

A good exercise is to open an interactive python console and type "open" + [ENTER]

>>> open
<built-in function open>

"open" is not a context manager object, but function. It doesn't have an enter method at all, instead it is defined in the following way;

@contextmanager
def open(..):
    ...

..you can define your own context manager functions in the same way, you can even override the definition of "open".

IMO though, the best thing to do if you need to create an object and then later use it as a context manager with arguments (..what I do) is to give the object a method that returns a temporary object that defines an enter method, like so;

class Comedian:
    def context(audience):
        class Roaster:
            context = audience
            def __enter__(self):
                audience = self.__class__.context
                # a comedian needs to know his/her audience.
        return Roaster(audience)

funny_thing = Comedian()
with funny_thing.context('young people') as roaster:
    roaster.roast('old people')

The order of the call-chain in this example is; Comedian.__init__() -> Comedian.context(args) -> Roaster.__enter__()

I felt like this answer was missing from the lot, so I added it.

EDIT: Added "return self" to Comedian.__call__ as pointed out by @MarkLoyman

Colostomy answered 20/5, 2020 at 18:3 Comment(4)
Awesome explication of the basic pieces of Python that taken together make all of this crystal clear.Johnsonian
I think you're missing a return self inside the def __call__(self, ...):.Rogerson
@MarkLoyman you're right thanks for pointing that out, I edited it in.Colostomy
Awesome, just what I was looking for. I have an advanced logger, and I want to document various stages of process that is being logged, by setting custom fields in logger, and then removing them when that stage is over. This allows to do it neatly and provide a good overview.Prismatoid
B
7

You can use the contextmanager decorator to pass arguments:

https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager

from contextlib import contextmanager

@contextmanager
def clippy_runner(*args):
    yield

IMHO, I find confusing that using contextmanager you can provide arguments, but you cannot provide them to __enter__

Brotherhood answered 16/12, 2016 at 16:34 Comment(1)
I second this. I have some settings that are relevant only inside the context. Passing them to __init__ is silly.Pothunter
A
3

Wouldn't you just pass the values to __init__ via the class constructor?

Afflatus answered 24/2, 2011 at 19:42 Comment(0)
D
0

I think to use the contextlib.contextmanager(native package) is a good idea.

More details, see as follows.

a simple example

from contextlib import contextmanager


class Person:
    def __init__(self, name):
        self.name = name

    def say_something(self, msg):
        print(f'{self.name}: {msg}')

    @staticmethod
    @contextmanager
    def enter(name,  # <-- members of construct
              para_1, options: dict  # <-- Other parameter that you wanted.
              ):
        with Person(name) as instance_person:
            try:
                print(para_1)
                print(options)
                yield instance_person
            finally:
                ...

    def __enter__(self):
        print(self.name)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__')


with Person.enter('Carson', para_1=1, options=dict(key='item_1')) as carson:
    carson.say_something('age=28')
    print('inside')
print('outside')

output

Carson
1
{'key': 'item_1'}
Carson: age=28
inside
__exit__
outside

example of yours

from typing import Union
from contextlib import contextmanager


def main():
    with ClippyRunner.enter(filename="clippytest/Test.xlsx",
                            param_dict='DATASOURCES[STAGE_RELATIONAL]') as clippy_runner:
        clippy_runner.do_something()


class ConnectBase:
    def connect(self):
        print(f'{type(self).__name__} connect')

    def disconnect(self):
        print(f'{type(self).__name__} disconnect')


class ExcelConnection(ConnectBase):
    def __init__(self, filename):
        self.filename = filename


class SQLConnection(ConnectBase):
    def __init__(self, param_dict):
        self.param_dict = param_dict


class ClippyRunner:
    def __init__(self, engine: Union[ExcelConnection], db: Union[SQLConnection]):
        self.engine = engine
        self.db = db

    def do_something(self):
        print('do something...')

    @staticmethod
    @contextmanager
    def enter(filename, param_dict):
        with ClippyRunner(ExcelConnection(filename),
                          SQLConnection(param_dict)) as cr:
            try:
                cr.engine.connect()
                cr.db.connect()
                yield cr
            except:
                cr.release()  # disconnect
            finally:
                ...

    def __enter__(self):
        return self

    def release(self):
        self.engine.disconnect()
        self.db.disconnect()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()


if __name__ == '__main__':
    main()

output

ExcelConnection connect
SQLConnection connect
do something...
ExcelConnection disconnect
SQLConnection disconnect

About contextmanager

A context manager does (basically) three things:

  1. It runs some code before a code block.
  2. It runs some code after a code block.
  3. Optionally, it suppresses exceptions raised within a code block.
Damascus answered 29/4, 2020 at 7:54 Comment(0)
T
0

You can save the state in the instance: (PS I don't recommend this as it leads to spaghetti code)

class Thing:

    def __init__(self):
        self.name = 'original'

    def __call__(self, name):
        self._original_name = self.name
        self.name = name
        return self

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        self.name = self._original_name

Here is the test:

instance = Thing()
assert instance.name == 'original'
with instance('new name'):
    assert instance.name == 'new name'

assert instance.name == 'original'
Twitt answered 1/8, 2022 at 19:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.