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.
  • Eugene

    good post! Thanks!

  • Phillip Condreay

    I think you’re missing the point here a bit. The “One Way to do it” mindset stems from PEP 20 (The Zen of Python), which is more of a mission statement than an explicit rule. The idea of it is not that there are no aliases or that two code pieces can’t be semantically different and produce the same result, but that there should be a single obvious approach to take to resolve every situation.

    Take for example your “Copying a list” segment above. While all four of those do produce identical input and output, the methods mean different things and would allow you to do different things. The first is a simple slice, the second constructs a list object using the copy constructor, the third uses a list comprehension, and the fourth uses pickle and the __getstate__ and __setstate__ methods. You can make the input and output differ for these methods by subclassing list. Ergo, while they produce identical output, they are not the same.

    I should also add that while the __repr__ and __hash__ methods of float and Decimal are the same, float and Decimal objects are quite different internally. `float(“1.”) is Decimal(“1.”)` evaluates to False.

  • Taylor Marks

    Here’s the issue with Ruby’s usage of extra things that mean identical or opposite things: as someone reading Ruby code and not having a lot of experience with it, I have to go look it up and discover that it means the same thing as something else. In Python, it’s been my experience that the way the “One Way to Do It” mission statement is often implemented instead by introducing as few new things as possible. IE, “unless” isn’t necessary because “if” and “not” already exist. “? :” isn’t necessary because “if else” already exists.

  • A “for” loop isn’t necessary becasue you could use a “while” loop instead. Would Python be better off without a “for” loop too?

  • Taylor Marks

    Python doesn’t have “for …” loops. They have “for … in …” loops which serve a very different purpose from a “while …” loop. A “while” loop is the One True Way (TM) to repeat a task until a condition is met, while a “for in” loop is the One True Way for performing an operation on every item in the collection. Although their implementations may be similar (I’m not certain – I’ve never looked at the source of the Python interpreter), their usages are not.

  • Taylor Marks

    Having said this, I wish Python could alert me if a “while” loop didn’t have any apparent way of exiting, and also had an explicit “forever” loop, which lacks any way of exiting besides ending the program (which wouldn’t be frequently used, but would always say to future readers that a loop never exits because it’s never supposed to, like a runloop, as opposed to a “while True:” loop.

  • Ali Hussain

    You can’t get what you’re looking for. What you’re asking for is the halting problem. And that is defined to be not computable. I do see you point about a forever statement, but I think the readability over ‘while True’ is less than the overhead of learning a new piece of the language.

  • Taylor Marks

    The halting problem just says that we can’t make a perfect while loop checker. You could write a simple checker that does something like:
    – Check for a break statement in the loop.
    – Check whether a variable in the condition of the loop is ever assigned to within the loop.
    If neither of those things are found, it could emit a warning that says “This loop may not have an ending”. It could be added to a python lint checker.

    Actually… I wonder if it’s possible to write a turing complete language that only has “for … in …”, “while …”, and “forever” loops…

This site uses cookies. Find out more about cookies.