1
\$\begingroup\$

I wanted to ask this here because what I wanted to originally do felt like a really Pythonic method. I want to be able to use the syntax:

d = {'apple':{'cranberry':{'banana':{'chocolate':[1,2,3,4,5,6]}}},'b':2}
with d['apple']['cranberry']['banana']['chocolate'] as item:
 for i in item:
 print(i)
 item.append('k')

but found that Python doesn't allow for using lists, dicts, etc. as context managers.

So I implemented my own:

def context_wrap(target):
 class ContextWrap:
 def __init__(self, tgt):
 self._tgt = tgt
 def __enter__(self):
 return self._tgt
 def __exit__(self, type, value, traceback):
 pass
 return ContextWrap(target)
if __name__ == '__main__':
 with context_wrap({'a':1}) as item:
 print(item['a'])
 with context_wrap([1,2,3,4,5]) as item:
 print(item[1])
 with context_wrap(3) as item:
 print (item)

In the above code, you can take any random object and wrap it inside an object that acts as a context manager controlling the underlying object. This means that inside any with clause, you can simply use the object with its alias. I feel like it looks a lot cleaner and clearer than something like:

alias = d['apple']['cranberry']['banana']['chocolate']
for i in alias:
 print(i)
alias.append('k')

I wanted to know if there was a more "Pythonic" way to do it. So the improvement that I'm looking for is in terms of better reliance on the standard Python library and/or in terms of my syntax.

Toby Speight
87.2k14 gold badges104 silver badges322 bronze badges
asked Feb 26, 2020 at 23:30
\$\endgroup\$
5
  • 5
    \$\begingroup\$ What's the advantage of this over just saying item = d['a']? Usually the purpose of a context manager is to do something in the __exit__ (close a file, free a resource, etc) but your use case doesn't require anything like that. \$\endgroup\$ Commented Feb 26, 2020 at 23:59
  • \$\begingroup\$ Welcome to code review, the question might be better received if the title was something like Context Manager Wrapper and there was a paragraph explaining what the code does. \$\endgroup\$ Commented Feb 27, 2020 at 0:23
  • 1
    \$\begingroup\$ @pacmaninbw Let me amend my post. \$\endgroup\$ Commented Feb 27, 2020 at 0:25
  • \$\begingroup\$ @SamStafford This is going to be used for more complicated objects where you wouldn't want to just endlessly list the same parameters over and over. Something like d = {'a':{'b':{'c':{'d':'e'}}}} where the object is complex/deeply nested. \$\endgroup\$ Commented Feb 27, 2020 at 0:25
  • \$\begingroup\$ You might want to read the guidelines for good questions at codereview.stackexchange.com/help/how-to-ask. \$\endgroup\$ Commented Feb 27, 2020 at 0:30

2 Answers 2

7
\$\begingroup\$

I think what you are doing is crazy, but yes, you can use Python’s library functions to clean it up.

from contextlib import contextmanager
@contextmanager
def context_wrap(target):
 yield target

Again, this is busy work.

alias = thing

is clearer, shorter and faster than

with context_wrap(thing) as alias:
answered Feb 27, 2020 at 3:33
\$\endgroup\$
4
  • \$\begingroup\$ Wow, this is extremely much cleaner than my solution. And I can see a few uses for it! Using a context manager doesn't leave a strong reference around in case something needs to be cleaned up by the garbage collector! I think it's especially useful for classes. \$\endgroup\$ Commented Feb 27, 2020 at 16:55
  • 2
    \$\begingroup\$ alias will still be equal to thing after the with statement exits. It doesn't remove the variable, so a strong reference will still exist! \$\endgroup\$ Commented Feb 27, 2020 at 17:11
  • \$\begingroup\$ I think I have some more work to do then. But thanks for the help anyway! \$\endgroup\$ Commented Feb 27, 2020 at 18:06
  • 2
    \$\begingroup\$ Trying to trick the Python garbage collector into cleaning things up at specific times is not going to be a fun game to play. Write it in Rust if object lifetimes are that important! :) \$\endgroup\$ Commented Feb 27, 2020 at 18:41
4
\$\begingroup\$

AJNeufeld provides the proper way to create small context managers using contextlib.contextmanager. While this is, for sure, the best way to accomplish your goal, I wanted to comment on the general use case of functions like the following:

def context_wrap(target):
 class ContextWrap:
 def __init__(self, tgt):
 self._tgt = tgt
 def __enter__(self):
 return self._tgt
 def __exit__(self, type, value, traceback):
 pass
 return ContextWrap(target)

With this sort of function, you are creating the class, instantiating it, and then throwing away the class definition on each function call. All this needs to be is the class definition, and then use that directly:

class ContextWrap:
 def __init__(self, tgt):
 self._tgt = tgt
 def __enter__(self):
 return self._tgt
 def __exit__(self, type, value, traceback):
 pass
with ContextWrap(target) as cw:
 # do something

Which is much lighter since you aren't re-creating resources all the time.

Any time you find yourself using that pattern, go simpler.

answered Nov 29, 2022 at 16:27
\$\endgroup\$
3
  • \$\begingroup\$ Would it be helpful to set self._tgt = None in the __exit__ method? This would ensure the reference was removed quickly. Or would that happen because the whole thing disappears when leaving the with statement? \$\endgroup\$ Commented Apr 3, 2024 at 2:10
  • \$\begingroup\$ Not quite, because the value tied to self._tgt is still referenced by the returned value after __enter__. For example, with ContextWrap('myval') as x: points the name x toself._tgt, so even if you set self._tgt to None, it would just remove it from the contextmanager's scope (inside the class), but wouldn't get rid of it entirely \$\endgroup\$ Commented Apr 3, 2024 at 2:23
  • 1
    \$\begingroup\$ Furthermore, the context manager itself is removed when the scope exits (after __exit__), so the self._tgt = None is just busy work \$\endgroup\$ Commented Apr 3, 2024 at 2:26

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.