11

If I was to write a file with this content:

#You have been defeated!
#It's merely a flesh wound!
We are the knights who say Ni!
We are the knights who say Ni!
We are the knights who say Ni!

Would it then be very non-pythonic to do it with a generator using send? I have never seen generators used like this elsewhere.

def write(file, header):

    with open(file,'w') as f:
        f.write(header)
        line = (yield)
        while True:
            f.write(line)
            line = (yield)

    return

file='holygrail.txt'
header="#You have been defeated!\n#It's merely a flesh wound!\n"
generator = write(file,header)
generator.send(None)
for i in range(3):
    generator.send('We are the knights who say Ni!\n')
generator.close()

I am asking, because the method above would be hugely beneficial to me instead of opening multiple different file streams in a contextlib stack. I would not have to use the contextlib module at all, if I write my files like this.

I have never asked a question like this before, and I don't know, whether it belongs on stackoverflow or not.

tommy.carstensen
  • 8,962
  • 15
  • 65
  • 108
  • 3
    I did not know generators could be used like this! And I don't know why this was down voted. – Jayanth Koushik Mar 27 '14 at 13:46
  • 2
    [What topics can I ask about here?](http://stackoverflow.com/help/on-topic) – Bonifacio2 Mar 27 '14 at 13:47
  • I have a couple of concerns: one, how do you stop the iteration, that is, when does the file get closed? Two, the `generator.send(None)` seems unnecessary if you swap the order of the two lines in the `while True` loop and remove the first `line = (yield)`. – chepner Mar 27 '14 at 13:47
  • That's a great idea but read the comment of @chepner carefully. – ElmoVanKielmo Mar 27 '14 at 13:49
  • @chepner Can I just stop the iteration with generator.close()? I just edited the question and added this. According to the documentation http://docs.python.org/3/reference/expressions.html#generator.send "When send() is called to start the generator, it must be called with None as the argument, because there is no yield expression that could receive the value." – tommy.carstensen Mar 27 '14 at 13:50
  • 5
    I think `file` isn't a very fortunate choice for arguments/variables, since it's a [builtin function](http://docs.python.org/2.7/library/functions.html#file). – Frerich Raabe Mar 27 '14 at 13:51
  • @tommy.carstensen I'd say that bit of overhead alone makes this use of a generator unpythonic. – chepner Mar 27 '14 at 13:52
  • 1
    @tommy.carstensen, since you have working code, I would suggest you ask this kind of question on codereview.stackexchange.com/ – wnnmaw Mar 27 '14 at 13:53
  • 1
    @wnnmaw Thank you! I didn't know about codereview.stackexchange.com. I'll see if I can transfer the questions. – tommy.carstensen Mar 27 '14 at 13:58
  • 4
    @FrerichRaabe: actually, `file` *isn't* a built-in function in Python 3, see [here](http://docs.python.org/3.4/library/functions.html). So now I'd argue it's often the best name, or at least a decent one. – DSM Mar 27 '14 at 14:12
  • @FrerichRaabe: even in Python 2, you should use `open` instead of `file` unless in very rare case then you need `isinstance` or a subclass. – jfs Mar 27 '14 at 19:04

4 Answers4

10

I like the creativity of your solution, but my subjective opinion would be that using contextlib.ExitStack() will look cleaner, be more readable, than using the generator since each generator would need to be primed with generator.send(None) and explicitly closed.


By the way, (even though I think contextlib will lead to shorter, more readable code), write could be simplified a little bit:

def write(file, header):
    with open(file, 'w') as f:
        f.write(header)
        while True:
            line = (yield)
            f.write(line)
    return

Note you only need one line = (yield) instead of two.

Also, instead of priming the generator with generator.send(None) you could use the coroutine decorator:

def coroutine(func):
    """ http://www.python.org/dev/peps/pep-0342/ """
    def wrapper(*args, **kw):
        gen = func(*args, **kw)
        gen.send(None)
        return gen
    return wrapper

This is a commonly understood idiom (PEP0342, David Beazley talk) for turning a generator into a coroutine. So decorating your generator with it would also serve the purpose of advertising that write is a coroutine.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Would it be sufficient to do generator.close() ? – tommy.carstensen Mar 27 '14 at 13:48
  • @tommy.carstensen: Yes, that would work too, but then is that really "Pythonic"? There does not seem be any benefit over `f=open(...); f.write(...); f.close()`. – unutbu Mar 27 '14 at 13:55
  • Wouldn't I have to do with contextlib.ExitStack() as stack: f = stack.enter_context(open(...)) for execution of clean up code ? I use contextlib, because I need to open multiple files. It would be very elegant, if I could just open each file inside a generator. – tommy.carstensen Mar 27 '14 at 14:06
  • I just saw your edit. I didn't know, I could just do "with f, g1, g2". It still gets ugly though, if I have to open hundreds of files. – tommy.carstensen Mar 27 '14 at 14:09
  • 1
    I like the creativity of your solution, but my subjective opinion would be that using `contextlib.ExitStack()` will look cleaner, be more readable, than using the generator since each generator would need to be primed with `generator.send(None)` and explicitly closed. – unutbu Mar 27 '14 at 14:29
  • Thank you unutbu. I will revert back to contextlib. I will accept this thread as the answer. Thanks! – tommy.carstensen Mar 27 '14 at 14:34
0

I think this question is somewhat subjective, but I believe "Pythonic" also means keeping it simple. And for your particular case, that would probably something along the lines of

open("blah.txt", 'w').write("""\
#You have been defeated!
#It's merely a flesh wound!
We are the knights who say Ni!
We are the knights who say Ni!
We are the knights who say Ni!
""")

I guess your actual case is different though...

Frerich Raabe
  • 90,689
  • 19
  • 115
  • 207
0

You're code is no shorter, and is no clearer, than

file='holygrail.txt'
header="#You have been defeated!\n#It's merely a flesh wound!\n"
with open(file, w) as fh:
    fh.write(header)
    for i in range(3):
        fh.write('We are the knights who say Ni!\n')

so I'm not sure what the benefit is.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I guess the problem is that the real input is coming from elsewhere. – ElmoVanKielmo Mar 27 '14 at 13:51
  • Yes, the real input is coming from elsewhere. The header from one file, which can easily be kept in memory and the data/body from multiple other files, which are all too large to be held in memory. – tommy.carstensen Mar 27 '14 at 13:55
0

The point of coroutines is in saving internal state between .send() calls.

Coroutines are often used to implement "consumer" pattern (I'm seldom using them to wrap xlwt worksheets: I need to track number of rows to flush them). Without any state you can use a simple function without any state (or .write() method of file object)

x3al
  • 586
  • 1
  • 8
  • 24