Generator combined with with/using statement - Python vs C#

Posted on Fri 30 December 2011 in Coding

The other day, I tried to return a generator expression from within a with statement in Python, like so:

#!/usr/bin/python

def readlines(fn):
    with open(fn) as fd: # <-- the with statement
        return (line for line in fd) # <-- the generator expression

if __name__ == "__main__":
    for line in readlines("/proc/partitions"):
        print line.strip()

Running the script above results in a nasty error:

$ ./genwith.py
Traceback (most recent call last):
  File "./genwith.py", line 8, in
    for line in readlines("/proc/partitions"):
  File "./genwith.py", line 5, in
    return (line for line in fd)
ValueError: I/O operation on closed file

I wanted to see how the same combination of mechanisms works in C# (which has using instead of with):

using System;
using System.IO;
using System.Collections.Generic;

class Program {
    private static IEnumerable ReadLines(string fn) {
        using (var reader = new StreamReader(fn)) { // <-- the using statement
            string line;
            while ((line = reader.ReadLine()) != null)
                yield return line; // <-- makes ReadLines a generator method
        }
    }
    public static void Main(string[] args) {
        foreach (var line in ReadLines("/proc/partitions"))
            Console.WriteLine(line);
    }
}

And this actually works very well:

$ dmcs genuse.cs
$ ./genuse.exe
major minor  #blocks  name

   8        0   20971520 sda...

But to be fair, the C# code doesn’t use a generator expression - it uses the yield statement which converts the entire ReadLines method into a generator. So it’s not an adequate comparison at all! Let’s go back to Python and use yield there as well:

#!/usr/bin/python

def readlines(fn):
    with open(fn) as fd: # <-- the with statement
        for line in fd:
            yield line # <-- makes readlines a generator function

if __name__ == "__main__":
    for line in readlines("/proc/partitions"):
        print line.strip()

…and now the result is much better:

$ ./genwith.py
major minor  #blocks  name

   8        0   20971520 sda...

Lesson learned: Use generator expressions with caution and use yield when a context manager (which is what the with statement uses) is involved.