One way to do it? (Ruby vs Python)
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.