Ruby-colored Blocks in Python 3
Cat.: Python, Ruby12. July 2005
Ruby-styled blocks in Python have been something to be a bit anxious about. After spending even a little time working with Ruby, it’s really hard to get the idioms that blocks facilitate out of your head. Dealing with certain types of problems with coroutines just feels right:
File.open("bla.txt") do |file|
file.read()
end
With the equivalent Python being roughly:
file = open('foo.txt')
try:
file.read()
finally:
file.close()
Eewww. What attracted me to Python was its ability to make code read very closely to its intent as it would be described by a human. Resource aquisition/release is one of those things that I’d rather not have to read about when I’m trying to extract the essence of what a piece of code is doing.
Of course, you can get close in most cases by using normal callable passing…
def with_open_file(file, block):
try:
block(file)
finally:
file.close()
def do_stuff(file):
file.read()
with_open_file(open('bla.txt'), do_stuff)
… but the Ruby block syntax reads better to these eyes and it seems I’m not the only one. A few weeks ago, Guido had this to say about the block style in relation to recent PEP activity:
That was all before I (re-)discovered yield-expressions (in Ruby!), and mostly in response to the most recent version of PEP 288, with its problem of accessing the generator instance. I now strongly feel that g.next(EXPR) and yield-expressions are the way to go.
So I’ve been following the succession of PEPs that have led us to PEP-342, Coroutines via Enhanced Generators, and PEP-343, Anonymous Block Redux and Generator Enahncements, with great interest.
It looks like there’s a decent chance that we’ll be able to stuff like this in Python 2.5:
@with_template
def opening(filename, mode="r"):
file = open(filename, mode)
try:
yield file
finally:
file.close()
with opening("/etc/passwd") as file:
file.read()
This is accomplished not by adding an implicitly passed block construct
like Ruby’s &block
, but by adding a basic message passing protocol for
generators. Generators will have two new methods: send
and
throw
. The important one here is throw
, which tells the generator to
raise an Exception
at its suspension point. All of this is hidden
behind the implementation of with_template
(sample implementation in
PEP-342).
What I’m interested in understanding more fully is how the PEP proposed
enhancements to generators will work in iterative cases or whether
that’s planned at all. The examples seem targeted toward resource
aquisition/release (which is fine, there’s definitely a strong set of
use cases in that area). But I’m interested in understanding how cases
similar to Ruby’s IO.foreach
will be handled:
IO.foreach("testfile") { |line| puts line }
Note that this is different from Python’s…
for line in open('testfile').readlines():
print line
… for a few reasons. First, resource aquisition/release is handled
within IO.foreach
where in the preceding Python snippet, it isn’t
really handled at all. Second, IO.foreach
reads the file iteratively,
calling the block each time, where as an approach using Python
generators, such as follows…
def lines(filename):
f = open(filename)
try:
line = f.readline()
while line:
yield line
line = f.readline()
finally:
f.close()
for line in lines('foo.txt'):
raise Exception("this doesn't work")
… falls apart because the exception raised in the iterating block is not automatically signaled back to the generator. There’s no guarantee that the finally block will execute when the iterating block exits.
I have a feeling there’s some aspect of this that I’m not fully grasping. Perhaps Phillip (PING) or someone else with a good understanding of the proposed generator enhancements can stop by and comment; inquiring minds want to know…