Sunday, October 16, 2011

Ruby vs. Python Briefly

Ok, so I figure it's about time to live up to the title of this blog, since I've spent the vast majority of the language discussion firmly planted in parentheses. Aside from the fact that my company is starting a project in Erlang, I've also been scripting Python and Ruby pretty heavily.

They're actually not very different languages. Neither is perfect from my perspective[1], and neither sucks. If I had to, I could get work done in both (and having gone through the Ruby chapter in 7 Languages in 7 Weeks, I'm more inclined to look at Ruby for my next big project than I used to be). To start with, here's a boiled down, no-nonsense table that represents my perspective.

...is more annoying than...

len([1, 2, 3])
[1, 2, 3].length
"foo " + bar + " baz"
or
"foo %s bar" % bar
"foo #{bar} baz"
", ".join(["one", "two", "three"])
["one", "two", "three"].join ", "
map(lambda a: a + 1, [4, 3, 2, 1])
## still makes more sense 
## than join or len, though
[4, 3, 2, 1].map {|a| a + 1}
a = [4, 3, 2, 1].sort()
a[0]
[4, 3, 2, 1].sort[0]
nothing.jpg foo.methods.sort
require 'optparse'
require 'pp'
require 'fileutils'
import optparse, fileutils
## I also prefer the more granular 
## symbol access I get with python
sudo apt-get install ruby-full
irb
python

...is about as annoying as...

def aFunction(foo, bar):
    #do stuff
    return baz
def a_function(foo, bar)
  #do stuff
  baz
end
with tempfile.NamedTempFile() as tmp:
    tmp.write("Test test\n")
    ##more stuff    
    tmp.flush()
    popen(["lp", "-d", "a-printer", tmp.name()])
Tempfile.open() do |tmp|
   tmp.write("Test test \n")
   ## more stuff
   tmp.flush
   system("lp", "-d", "a-printer", tmp.name)
end

So I am slightly biased, but like I said earlier, not enough to actually decry either language. The biggest point in Ruby's favor is its handling of blocks (as seen in that tempfile pseudo-code). I like having an expression that says "Create an entity, do this stuff and then clean up", without having to clean up myself. Python doesn't like that.[2] Gotta admit, I boggled at the join syntax the first time around. Rhetorically, who the hell decided it makes sense that a join operation is something you do to the delimiter, rather than the list? In my opinion, it would even make more sense to make it a standalone function a-la len.

I really like the syntactic whitespace in Python.

def something():
    foo()
    bar()
seems like it's cleaner than the Ruby equivalent. Except that when I want to return the result of bar (which I do quite often, given that I much prefer functional programming to OO), I need to do so explicitly. Ruby has me waste an additional line on end, but returns implicitly. While I'm at it, Python libraries seem to be heavily anti-functional programming. They do the standard "OO" thing of exposing functionality via classes, but they also typically have a heavy reliance on side effects, which makes it harder than it ought to be to compose things. A recent example I had to go through involved using pyPDF and reportlab to process existing PDFs. You can do it, but the amount of fiddling involved is nontrivial if you want to decompose the problem properly because you need to do so by setting up multiple instances of PdfFileReader/canvas and making destructive changes to them.

Also, not represented in the table is how much easier it is to install python packages in Debian. While gem frequently errors on something, I've yet to find a package I need that I can't either apt-get or retrieve using python-setuptools. That's worth something (in fact, it's worth enough that I've been procrastinating on a ruby port of get-youtube-series, which used only default components in Python, but requires several installs in Ruby).

The last thing that table doesn't encompass is the version situation. That's a fairly major one from my perspective, but I'm not sure how serious it actually is. Python 3 has been out for quite a while, but it's not uncommon to see "Supports Python 2.7" on various frameworks/utilities. Squeeze still provides 2.6.6, Django still requires 2.[4-7] and Google app-engine is still asking for 2.5 (with 2.7 being supported as an "experimental" feature). That's less than encouraging. By contrast, Ruby 1.9 is fairly widely supported (though the Debian repos are still at 1.8.7). That just doesn't seem to bode well for the next version, regardless of how enthusiastic Rossum is about it.


Footnotes

1 - [back] - Though, to be clear, my opinion is that Ruby gets a damn sight closer than Python.

2 - [back] - Thank you Brendan Miller for pointing me to the with statement (documented here, here and here) which does emulate blocks well enough for my purposes.

3 comments:

  1. For the tempfile thing, you should use python's with statement. It's actually very similar to lisp's with-* macros like with-open-file, but a little more flexible since it defines a general interface for releasing any resource...

    with tempfile.NamedTemporaryFile() as tmp:
    tmp.write("Test test\n")
    tmp.flush()
    subprocess.popen(["lp", "-d", "a-printer", tmp.name()])

    This will of course, properly clean up in the face of an exception.

    ReplyDelete
  2. @Brendan Miller: Thanks! Updated the article to reflect that point.

    ReplyDelete
  3. I took another look. You should also replace:

    map(lambda a: a + 1, [4, 3, 2, 1])

    With [a + 1 for a in [4, 3, 2, 1]]

    map isn't really used much in python because list comprehensions/generators do the same job better.

    The first time I saw the loop macro in CL, I thought of how similar it is to python comprehensions and generators.

    ReplyDelete