First thoughts on Ruby: A quick showdown vs. Python

Contents

Python and Ruby always struck me as somewhat related. They’re both dynamic languages from the 1990s, still highly popular, and influenced a number of more modern dynamic programming languages like Julia. They’re similar enough in spirit that one can easily fall into the trap of thinking that if they know one of Python and Ruby, they don’t have to learn the other. I was primarily a Python programmer for my research work, and Python is popular in academia, so I am definitely guilty of having a closed mind toward Ruby.

Today I made an effort to challenge my assumptions and give Ruby a serious shot. I finished the Ruby koans, which took me about five hours, and four hours of real work. I’d messed with Try Ruby before that, but that didn’t take me very far, and RubyMonk felt too slow and clunky for my tastes when I tried the primer.

So the natural thing to do for me is to compare Ruby with Python, and see what I like and don’t like about each of them. I’m not trying to start a flamewar, it’s more like I’m just trying to translate some of my knowledge and highlight some of the differences that I saw so far.

Toy script comparisons

I wrote some toy scripts to show the most jarring differences so far. Yeah, I’m sure I’ll dig further into the language and all of this code will look silly, but as a budding Rubyist this is about as advanced as I can muster.

Running sum

In Ruby, the tendency is to iterate over a number of variables with the #each method, and then you pass in a block that gets called on each of the elements. What’s immediately noticeable here is that Ruby blocks are way more powerful than Python’s anonymous callalbles (lambdas).

#!/usr/bin/env ruby

running_sum = []
args = []

[1,5,7].each do |x|
  args << x
  if running_sum.size > 0 then
    running_sum << running_sum.last + x
  else
    running_sum << x
  end
end

puts "Running sum: #{running_sum} from #{args}."
#!/usr/bin/env python3

running_sum = []
args = []

for n in [1, 5, 7]:
    args.append(args)
    if running_sum:
        running_sum.append(running_sum[-1] + n)
    else:
        running_sum.append(n)

print("Running sum: {} from {}.".format(running_sum, args))

At face value, the two aren’t very different, but these examples illustrate a fundamental difference between the attitudes toward iteration: Python uses for loops for everything, but Rubyists will do everything in their power to avoid using them.

OOP

Ruby classes seem to be both less verbose and syntactically simpler than Python classes. Both languages have a message passing way of calling instance methods: in Python, with getattr(object, name) and in Ruby via #send(). Mixins, via modules, are the coolest advantage Ruby seems to have over Python, where the closest equivalent is boring old implementation inheritance.

Both languages have an equivalent for getters and setters. In Python, this is done via a decorator, but in Ruby it seems to be a language builtin.

#!/usr/bin/env ruby

module Nameable
  def name=(name)
    puts "changed name to #{name}"
    @name = name
  end

  def name
    @name
  end
end

class Dog
  include Nameable

  def initialize(name)
    @name = name
  end

  def bark
    puts 'Woof!'
  end
end

fido = Dog.new('Fido')
fido.send('bark')
fido.name = 'Cheese'
puts fido.name
#!/usr/bin/env python3

class Nameable(object):
    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        print('Changed name to {}'.format(value))
        self._name = value

class Dog(Nameable):
    def __init__(self, name):
        self._name = name

    def bark(self):
        print("Woof!")

fido = Dog('Fido')
fido.bark()
fido.name = 'Cheese'
print(fido.name)

Hidden

Finally, a serious scripting task!

I’ve been playing with 2bwm as my window manager lately. One of its major defects is that it lacks an easy way to list all hidden windows and show them. It comes with a tool, hidden, that will list hidden windows and print xdotool output for raising them, but the output looks like this:

'hidden.rb':'xdotool windowactivate 0xc003ec windowraise 0xc003ec'
'[nathan@dionysus][~/dotfiles]%':'xdotool windowactivate 0xc0085c windowraise 0xc0085c'

Which obviously isn’t sufficient for easily raising the windows. A potential fix to this is building a wrapper for dmenu that lets you easily raise hidden windows.

#!/usr/bin/env ruby

hidden_windows = `hidden -c`.split(/\n/)

map = {}
hidden_windows.each do |line|
    match = /'(.*)':'(.*)'/.match(line)
    key = match[1]
    val = match[2]
    while map.has_key? key
        key = key + '*'
    end
    map[key] = val
end

choice = nil

IO.popen('dmenu', 'r+') do |dmenu|
    map.keys.each do |name|
        dmenu.write(name)
        dmenu.write("\n")
    end
    dmenu.close_write
    choice = dmenu.gets.rstrip
end

IO.popen(map[choice])

In Python:

#!/usr/bin/env python3

import subprocess
import re

def main():
    hidden_windows = subprocess.check_output(['hidden', '-c'])
    hidden_windows = hidden_windows.splitlines()
    hidden_map = {}
    search = re.compile("^'(.*)':'(.*)'$")
    for window in hidden_windows:
        window = window.decode('utf-8')
        match = search.match(str(window))
        name, tool = match.group(1), match.group(2)
        while name in hidden_map:
            name += '*'
        hidden_map[name] = tool
    dmenu = subprocess.check_output(['dmenu'],
            input=bytes('\n'.join(hidden_map.keys()), encoding='utf-8'))
    subprocess.call(hidden_map[dmenu.rstrip().decode('utf-8')], shell=True)


if __name__ == '__main__':
    main()

The advantages Ruby has here are its simpler means of spawning and interacting with subprocesses, and the far prettier Regex syntax, both of which are inspired by Perl. Python, by contrast, has a clunkier regex engine that requires an import, a call to re.compile, and another call to regex.match(...) in order to use it.

The other thing you notice here is the bytes handling. Subprocess output returns bytes objects in Python, and they have to be decoded into utf-8 before I can do much with them.

This results in a far more verbose Python solution.

It’s also worth mentioning that the subprocess interface in Ruby is handled through an RAII-style block. In Python’s case, it looks like Popen objects can be used in with statements:

with Popen(["ifconfig"], stdout=PIPE) as proc:
    log.write(proc.stdout.read())

but that leaves the clunky Popen interface to worry about. Personally I can never remember what stdout=PIPE actually does, or which of those given modes I should be using for my process. In Ruby, I get to use fopen(3) arguments to describe how I’d like to handle fds on the subprocess. That’s an interface I’m already familiar with, and it’s intutitive.

Conclusion

My initial impression of Ruby is quite positive. It’s a strong candidate for replacing my one off Python scripts that often felt a bit too verbose for the tasks they were achieving. While I’m not comfortable enough to do serious development in it yet, I do get the feeling that Ruby will be quite readable over the equivalent Bash/Awk/Sed mashup that I might otherwise use to avoid writing a wordy Python script that uses subprocess.

There’s a distinct impression that Ruby is more fun than Python. There are many ways to achieve certain tasks, and the general goal is to find the most beautiful way. In Python, it seems like there’s a heavier focus on readability. I was skimming through some of the Rails codebase, and Rubyists seem to have no qualms about using the obscure ||= operator, in spite of the fact that it really doesn’t mean what you think it means. I think the most obscure syntactic construct in Python is just the wacky ternary operator.

I do not believe that Python and Ruby are really playing in the same niche. While Python dominates in academia and scientific programming, Ruby seems to win in the Perl-ish systems scripting and web development areas (though Django and Tornado put out some serious competition with Rails and Sinatra, and Python’s Pelican looks like a fine alternative to Jekyll).