Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle both `with open(...)` and `sys.stdout` nicely?

Tags:

python

Often I need to output data either to file or, if file is not specified, to stdout. I use the following snippet:

if target:
    with open(target, 'w') as h:
        h.write(content)
else:
    sys.stdout.write(content)

I would like to rewrite it and handle both targets uniformly.

In ideal case it would be:

with open(target, 'w') as h:
    h.write(content)

but this will not work well because sys.stdout is be closed when leaving with block and I don't want that. I neither want to

stdout = open(target, 'w')
...

because I would need to remember to restore original stdout.

Related:

  • Redirect stdout to a file in Python?
  • Handling Exceptions - interesting article about handling exceptions in Python, as compared to C++

Edit

I know that I can wrap target, define separate function or use context manager. I look for a simple, elegant, idiomatic solution fitting that wouldn't require more than 5 lines

like image 760
Jakub M. Avatar asked Jul 11 '13 20:07

Jakub M.


4 Answers

Just thinking outside of the box here, how about a custom open() method?

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename=None):
    if filename and filename != '-':
        fh = open(filename, 'w')
    else:
        fh = sys.stdout

    try:
        yield fh
    finally:
        if fh is not sys.stdout:
            fh.close()

Use it like this:

# For Python 2 you need this line
from __future__ import print_function

# writes to some_file
with smart_open('some_file') as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open() as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open('-') as fh:
    print('some output', file=fh)
like image 187
Wolph Avatar answered Nov 18 '22 08:11

Wolph


Stick with your current code. It's simple and you can tell exactly what it's doing just by glancing at it.

Another way would be with an inline if:

handle = open(target, 'w') if target else sys.stdout
handle.write(content)

if handle is not sys.stdout:
    handle.close()

But that isn't much shorter than what you have and it looks arguably worse.

You could also make sys.stdout unclosable, but that doesn't seem too Pythonic:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
like image 44
Blender Avatar answered Nov 18 '22 10:11

Blender


An improvement of Wolph's answer

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename: str, mode: str = 'r', *args, **kwargs):
    '''Open files and i/o streams transparently.'''
    if filename == '-':
        if 'r' in mode:
            stream = sys.stdin
        else:
            stream = sys.stdout
        if 'b' in mode:
            fh = stream.buffer  # type: IO
        else:
            fh = stream
        close = False
    else:
        fh = open(filename, mode, *args, **kwargs)
        close = True

    try:
        yield fh
    finally:
        if close:
            try:
                fh.close()
            except AttributeError:
                pass

This allows binary IO and pass eventual extraneous arguments to open if filename is indeed a file name.

like image 24
Evpok Avatar answered Nov 18 '22 10:11

Evpok


Why LBYL when you can EAFP?

try:
    with open(target, 'w') as h:
        h.write(content)
except TypeError:
    sys.stdout.write(content)

Why rewrite it to use the with/as block uniformly when you have to make it work in a convoluted way? You'll add more lines and reduce performance.

like image 10
2rs2ts Avatar answered Nov 18 '22 08:11

2rs2ts