fizwidget

cat /dev/random

Python or Ruby?

Python and Ruby are the two heavyweights in the general-purpose, high-level, dynamic language category. They’re both awesome, but given the choice, I’ll generally go with Ruby. This is my attempt at explaining why…

Ruby is consistently object-oriented

Pretty much everything we do in Ruby involves calling methods on objects:

1
2
3
4
5
6
7
8
-42.abs               # => 42
[1, 2, 3].length      # => 3
[1, 2, 3].reverse     # => [3, 2, 1]
[1, 2, 3].reverse!    # Reverses list in-place.
[1, 2, 3].min         # => 1
"foo".capitalize      # => "Foo"
File.new("bar")       # => File object
file.close            # Closes file object.

In Python though, a mixture of functions and methods are typically used:

1
2
3
4
5
6
7
8
abs(-42)              # => 42
len([1, 2, 3])        # => 3
reversed([1, 2, 3])   # => [3, 2, 1]
[1, 2, 3].reverse()   # Reverses list in-place.
min([1, 2, 3])        # => 1
"foo".capitalize()    # => "Foo"
open("bar")           # => File object
file.close()          # Closes file object.

One of the benefits of Ruby’s consistency is that we can always read code left-to-right. This makes functional-style pipelines much easier to read and write:

1
"world hello".split.reverse.join(" ") # => "hello world"

The equivalent Python code is much harder to make sense of. We have to read some parts left-to-right and other parts inside-out. Yuk!

1
" ".join(reversed("world hello".split())) # => "hello world"

Blocks == awesome

One of Ruby’s best features is the elegant syntax it has for passing anonymous functions (“blocks”) as arguments to methods. Blocks are like Python’s lambda expressions, only more powerful, more elegant, and more widely used.

Blocks allow us to perform many different tasks using a single uniform syntax. A few examples:

1
2
3
4
5
[1, 2, 3].each   { |x| puts x }         # Iteration using blocks.
File.open('a')   { |f| puts f.read }    # Automatic resource management using blocks.
[1, 2, 3].select { |x| x > 2 }          # List processing using blocks.
data.sort_by     { |x| foo.bar(x) }     # Sorting using blocks.
urls.lazy.map    { |u| download(u) }    # Lazy evaluation using blocks.

In Python, we’d typically use no fewer than five language constructs to perform the same tasks:

1
2
3
4
5
for x in [1, 2, 3]: print(x)            # Iteration using 'for'.
with f as open('a'): print(f.read())    # Automatic resource management using 'with'.
[x for x in [1, 2, 3] if x > 2]         # List processing using list comprehensions.
sorted(data, key=lambda x: foo.bar(x))  # Sorting using 'lambda'.
(download(u) for u in urls)             # Lazy evaluation using generator expressions.

Another thing to note is that Python’s lambda expressions are, as the name suggests, restricted to containing a single expression. Ruby’s blocks have no such restriction, which can be very convenient at times.

One flexible construct > multiple rigid constructs in my book!

Private parts

Instance variables are private by default in Ruby, which encourages proper encapsulation of implementation details.

1
2
3
4
5
6
7
class Foo
  def initialize
    @secret = 42
  end
end

Foo.new.secret # => Error!

Everything defaults to public in Python, and privacy can only be suggested by naming conventions. This means we’re more likely to end up with inappropriate dependencies on implementation details.

1
2
3
4
5
class Foo:
    def __init__(self):
        self.not_so_secret = 42

Foo().not_so_secret # => 42

Ruby’s approach seems the wiser one to me. Encapsulation is a Good Idea™, so why shouldn’t the language support and encourage it?

(By the way, Ruby’s approach doesn’t result in C++/Java-style boilerplate. We can generate default getters/setters by placing attr_accessor :property_name in the class body, and override them later if need be.)

Readability

OK, I’ll admit this one is ever so slightly subjective. Having said that, compare this:

1
2
3
4
5
6
7
8
9
10
11
12
class Person
  def initialize(age)
    @age = age
  end

  def to_s
    "Person is #{@age} years old."
  end
end

person = Person.new(34)
puts person

To this:

1
2
3
4
5
6
7
8
9
class Person(object):
    def __init__(self, age):
        self.age = age

    def __str__(self):
        return "Person is {} years old.".format(self.age)

person = Person(34)
print(person)

The Ruby code has fewer parentheses, colons, and underscores, and doesn’t need to specify object, self, or return. Python’s whitespace sensitivity does allow it to omit end, but this doesn’t make up for the other clutter in my opinion.

Loops and if-statements can also be more readable in Ruby. Compare these Python snippets:

1
2
3
4
5
6
7
8
9
10
if not some_sequence:
    # PEP 8 promotes this as the Pythonic way of checking for an empty sequence.
    # Whatever happened to "explicit is better than implicit"??

if some_number:
    # This checks for a non-zero number.
    # Admittedly it's not very Pythonic, but the language does support it.

while not buffer.is_full():
    # This doesn't exactly read very well...

To these Ruby ones:

1
2
3
4
5
6
7
8
9
10
11
12
if some_sequence.empty?
  # Such clarity, much obviousness.
end

if some_number.zero?
  # Ruby forces us to be much more explicit when checking for zero.
  # We could also use 'some_number == 0', but this nicer IMO.
end

until buffer.full?
  # Relative to the Python snippet, this reads like English prose!
end

Ruby supports unless and until (logical negations of if and while), allows question marks to appear in method names, and allows parentheses to be omitted when calling methods. Neat!

As mentioned in previous sections, consistent object-orientation and elegant block syntax also aid Ruby’s readability.

Naming and documentation

The naming conventions used in Python’s standard library are a bit of a mess (even PEP 8 admits as much). Ruby’s standard library is much more consistent in comparison.

In terms of documentation quality, Ruby also has the edge. Descriptions are clearer and more detailed, and usage examples are almost always given. Consider the documentation for str.capitalize in Python:

1
2
3
4
5
str.capitalize = capitalize(...)
    S.capitalize() -> string
    
    Return a copy of the string S with only its first character
    capitalized.

Then compare it with Ruby’s String#capitalize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
= String#capitalize

(from ruby core)
------------------------------------------------------------------------------
  str.capitalize   -> new_str

------------------------------------------------------------------------------

Returns a copy of str with the first character converted to uppercase
and the remainder to lowercase. Note: case conversion is effective only in
ASCII region.

  "hello".capitalize    #=> "Hello"
  "HELLO".capitalize    #=> "Hello"
  "123ABC".capitalize   #=> "123abc"

Unless you were paying close attention, you might have missed the fact that str.capitalize converts the remainder of the string to lowercase (e.g. “ABC” goes to “Abc”). The Ruby documentation makes this behaviour much more obvious, and gives clarifying examples.

This is just one example of course, but these kinds of differences are not atypical in my experience.

Conclusion

So those are a few of the reasons why I prefer Ruby to Python. Admittedly this was a rather one sided comparison – Ruby has plenty of downsides and Python plenty of upsides that I neglected to mention. Perhaps those will be the topic of a future post!

Comments