One way to do it? (Ruby vs Python)

Python vs Ruby

Python vs Ruby

Following on from my blog post about some of the differences I thought were important between Ruby and Python (senktec.com/2013/06/ruby-vs-python/), one of the things often cited as a difference between the two programming languages is that Python strives for one obvious way to do things, and Ruby strives to allow multiple ways to do things. I didn’t touch on this in my article as I don’t believe the differences in this regard for the everyday use of the languages match up to the differences in the rhetoric. So, it’s way down on my list of importance. I suggest for most people, it’s an easy fact that they read somewhere that they like to quote, rather than something they have found to be an important distinction after using both languages. It must be true because Ruby has unless, right?

Why have more than one way to do things?

Larry Wall (inventor of the Perl language) is cretited with a quote on this, “sometimes phrasing a sentence differently might make it’s meaning clearer” (c2.com/cgi/wiki?ThereIsMoreThanOneWayToDoIt). The ability for code to be read and understood is hugely important. If we can re-write some code so it is clearer or easier to understand the meaning behind it, we should ideally strive to make that change.

Both Ruby and Python programmers cite readability as a reason their language is better, they just disagree about what makes something readable.

Python programmer: “How can you possibly read that, it’s got unless statements in it?”

Ruby programmer: “How can you possibly read that, it’s got loads of if not‘s in it?”

More than one way in Ruby

There are a number of ways Ruby provides more than one way to do things, some of these include:

Conditionals

Ruby includes both if and unless, and may also be written on one line, alternatively there is also a ternary operator (? :) and case statements:

if n != 1
  puts "n is not 1"
end
unless n == 1
  puts "n is not 1"
end

puts "n is not 1" if n != 1

puts "n is not 1" unless n == 1

puts( n == 1 ? "n is 1" : "n is not 1" )

case n
when 1
  puts "n is 1"
else
  puts "n is not 1"
end

Aliasing methods

Such as size and length, or map and collect.

string = "hello world"
string.size
string.length

items = [1,2,3,4,5,6]
new_items = items.map{|x| x + 1 }
new_items = items.collect{|x| x + 1 }

Opposite methods

Such as select and reject, or any? and none?:

odd_items = items.select{|i| i % 2 != 0 }
odd_items = items.reject{|i| i % 2 == 0 }
items.any?
items.none?

In-place methods with !

Such as reverse and reverse!:

items = items.reverse
items.reverse!

Passing a block of code to a function

def func1 block
  block.call "world"
end
def func2 &block
  block.call "world"
end
func1 lambda {|text| puts "hello " + text }
func2 {|text| puts "hello " + text }

Accessing first item in an array

a = items.first
a = items[0]
a, = items

Incrementing a variable

a = 0
a += 1
a = a + 1

More than one way in Python (what?)

Ok, Python doesn’t have aliased method calls to the same functionality, or unless, but what about things like these?

Transforming each element of a list

items = [1,2,3,4,5,6]

items = [x + 1 for x in items]

items = map(lambda x: x + 1, items)

def func(x): return x + 1
items = [func(x) for x in items]

Copying a list

new_items = items[:]
new_items = list(items)
new_items = [x for x in items]
import copy
new_items = copy.copy(items)

Accessing the only item in a list

mylist = [2]
a = mylist[0]
a, = mylist
[a] = mylist

Incrementing a variable

a = 0
a += 1
a = a + 1

Conditionals

if n != 1:
  print("n is not 1")
else:
  print("n is 1")
print("n is 1" if n == 1 else "n is not 1")

Converting a string to a floating point number

value = float("123.456")
from decimal import Decimal
value = Decimal("123.456")

I haven’t just made all these up to argue a point. For example, there are plenty of accepted answers on stackoverflow.com that use different versions of much of the above showing they are actually being used in practice.

Algorithmic differences

Even if there is one, or few sensible ways to write the code, there are typically many different algorithms, or approaches to go about solving a problem. I don’t think it makes sense to obsess about one way to do it in the language, when there are many ways to go about solving the actual problem itself.

10 ways to write a factorial function in Python

I’m assuming we don’t have math.factorial().

def f1(n):
  if n > 1:
    return n * f1(n - 1)
  else:
    return 1

def f2(n):
  return n * f2(n - 1) if n > 1 else 1

def f3(n):
  if n <= 1: return 1
  else: return f3(n - 1) * n 

def f4(n, accumulator=1):
  if n == 0:
    return accumulator
  else:
    return f4(n - 1, accumulator * n)

f5 = lambda n: n and n * f5(n - 1) or 1

def f6(n):
  result = 1
  for i in range(1, n + 1):
    result *= i 
  return result

def f7(n): 
  result = 1 
  while n > 0:
    result *= n
    n -= 1
  return result

def f8(n):
  if n == 0:
    return 1
  else:
    return reduce(lambda a,b: a * b, range(1, n + 1))

def f9(n):
  numbers = xrange(1, n + 1)
  if len(numbers) == 0:
    return 1
  return reduce(int.__mul__, numbers)

def f10(n):
  if n == 0: return 1
  return eval('*'.join([str(i) for i in range(1, n + 1)]))

While some of these ways are not so obvious, there are clearly many sensible ways to implement this function in Python, as well as an infinite number of silly ways. (More can be found at artima.com/forums/flat.jsp?forum=181&thread=75931.) Most of these will have equivalents in Ruby and there may be a few other sensible ways in Ruby too. However, to simply say there’s only one way to do things like this in Python really doesn’t make any sense to me.

Unnecessary stuff adds complications

I think it should be about finding a balance between allowing the flexibility to express things in different ways, as I think that can be useful, and avoiding unnecessary duplication in the language, as we don’t want to learn everything twice.

Many Pythonists argue that .size aliased to .length is unnecessary clutter, and many Rubyists argue that the with statement is unnecessary clutter (just use a block).

There are some things in Ruby that I’d be in favour of removing, perhaps including the for loop (as .each is great) and reducing the number ways to define an anonymous function (i.e. lambda, proc, Proc.new).

What concerns me more, though, is whether things in the language will catch me out, or surprise me. In English the words ‘length’ and ‘size’ could both be used to describe the number of items in a collection, plus other programming languages use these terms too. I don’t think it reasonable for languages like Ruby or Python to use one one of these for the number of items, and the other to mean something else, like the size of an internal buffer used to store the items in memory. That would really catch people out. So, to avoid surprising people, either one of those words should never be used, or they should both return the same thing. In this case and in map/collect or inject/reduce I think it makes sense to return the same thing to allow the flexibility when writing code.

Conclusions

It’s clear that Ruby provides more ways to write the same code than Python, but on a sliding scale from one way to many ways, I don’t think the gap is as big as the rhetoric suggests. Despite some of Pythons followers claiming there is only one way to do things, I don’t think it lives up to that claim, and I don’t think it’s practically achievable or desirable.

If you enjoyed this post, consider leaving a comment or subscribing to the RSS feed.
This site uses cookies. Find out more about cookies.