DEV Community

Cover image for How would you rewrite this Python code?

How would you rewrite this Python code?

Dustin King on May 26, 2018

I ran across this problem while writing some code for a challenge. I have some code that I want to write to a particular file if a filename is pro...
Collapse
 
rhymes profile image
rhymes • Edited

I think the simplest is the first one :D

Or you can wrap that if in a normal function if you plan to use it more than once:

def file_or_stdout(fname=None):
  if fname:
    return open(fname, 'w')
  else:
    return sys.stdout

I don't think you need to use a context manager in this case because there are no other operations between the opening and closing of the file.

Anyway I wouldn't do it like that.

Keep in mind that if you use sys.stdout this way you will close it which means that any attempt to write on it after your with statement will result in an error:

>>> import sys
>>> with file_or_stdout() as f:
...     f.write("Yo!\n")
...
Yo!
4
>>> sys.stdout.write("Yo 2\n")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
>>> print("yo 3")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

So why do you need to hide stdout this way? It's probably easier to use StringIO, write to it and then use a print to display it on stdout.

Collapse
 
cathodion profile image
Dustin King

So why do you need to hide stdout this way?

I'm just trying to keep the code clear and non-repetitive. In terms of level of effort and maintainability, if it needs to be able to optionally write to a file, breaking the inner logic out into a function as inspired by @thinkslynk is probably best.

But if it's a one-off project it might be even better to only write to stdout and pipe it to a file in the shell if needed. (Sometimes it's fun to over-engineer though.)

Collapse
 
tadaboody profile image
Tomer

Actually the context manager solves this by only opening/closing the fp if it isn't to stdout so it might be the best solution

Collapse
 
rhymes profile image
rhymes

Oh yes, you're right :-)

+1 to the context manager, though it might be overkill if the function is used only once.

Collapse
 
thinkslynk profile image
Stephen Dycus

I think the cleanest way is this:

if filename:
    with (open(filename, 'w') as file:
        do_something(file)
else:
    do_something(sys.stdout)
Collapse
 
cathodion profile image
Dustin King

Certainly works if do_something is just a function call. I meant it as a stand-in for a larger block of code. But maybe that block should actually be a function.

Collapse
 
blubberdiblub profile image
Niels Böhm

Yes, it should be a function. stdout/stderr/stdin are different from other streams in that they're managed outside (by the OS and Python, not by your app) whereas an open should be used with with in simple cases if possible.

You can start treating them the same when you start writing to / reading from them and stop when you stop doing that. All that can nicely be stuck away in a function. That also enables you to call the actual I/O part with a different file-like thing, say a StringIO instance when testing.

Collapse
 
cathodion profile image
Dustin King

I don't think I'd heard of CQS before. I'll have to think more about it. It does make the code clear in this case.

On first examination, one issue I have with the general concept is that it seems that functions that act like constructors, such as file_or_stdout() in my example, or even just output = open(...) would violate it because they both do something (e.g. open the file, build a context manager) and return a reference to the thing they've opened or created.

CQS seems similar to a rule of thumb that parameters are for input and return values are for output, which I'm violating with do_something(), because it was a stand-in for a couple lines in the original code.

Maybe the larger takeaway of this exercise is that mutability has its pitfalls.

Collapse
 
welkensoyo profile image
Derek Bartron

How is the first one harder to read then the rest that more lines and more complicated? If a Python coder can't read an inline expression, how is turning it into a context manager helping? I'm confused by this entire article.

Collapse
 
cathodion profile image
Dustin King

There is a lot of subjectivity involved in deciding which code is "better", and that line may be perfectly readable for you.

To me it seemed like there was too much happening in that one line, which would mean someone has to stop on it for a few seconds to figure out what it's doing. Having a more complicated with clause also seemed to distract from the code after with(...):. This isn't shown very well here, since I just used do_something(file) as a stand-in, instead of the couple lines in the linked code this was derived from.

But you may have a point in that one or two long lines might be preferable to a lot of short lines. The point of this post was that I ran across a situation in which the clearest way to write the code wasn't obvious, and I thought it would be valuable to hear peoples opinions on different ways to do it, and the tradeoffs between them.

Collapse
 
yorodm profile image
Yoandy Rodriguez Martinez • Edited

I'll tell you as my old Python sensei used to tell me: When in doubt, import this.
I think your second solution is the one. It is both concise and does not messes with PEP 8.

Collapse
 
cathodion profile image
Dustin King

What do you mean by "import this"?

Collapse
 
pbouillon profile image
Pierre Bouillon

Just type "import this" I'm your interpreter, you will understand :)

Thread Thread
 
cathodion profile image
Dustin King

Ah yes, the good ol' Zen of Python.

Collapse
 
tadaboody profile image
Tomer

TIL building a context manager is so simple and clean.