Skip to main content

Ruby: A Python Programmer's Perspective Part III

This is a somewhat random list of things that were interesting or surprising to me when I read Ruby for Rails. The previous post in the series is Ruby: A Python Programmer's Perspective Part II.

I've mentioned before that Ruby has symbols which are similar to, but distinct from, strings (which are mutable). The syntax is :foo. To create a symbol with spaces, use :"foo bar".

Ruby has two syntax for ranges. ".." includes both endpoints. "..." does not include the right endpoint and thus behaves like slices in Python do:
>> (0...2).each {|x| p x}
=> 0...2

>> (0..2).each {|x| p x}
=> 0..2
Since the "+" operator is just syntactic sugar for the "+" method, the following are equivalent:
>> 1 + 1
=> 2
>> 1.+(1)
=> 2
Whenever possible, I think interpreters should raise an exception if you misspell something. Hence, I found the following interesting:
>> def f
>> p @undefed_instance_variable # This prints nil.
>> p undefined_local # This raises a NameError.
>> end
=> nil
>> f
NameError: undefined local variable or method `undefined_local' for main:Object
from (irb):39:in `f'
from (irb):41
nil and false are the only two objects that evaluate to false:
>> if []: puts "Still true"; end
Still true
=> nil
In Python, "truthiness" is a lot more complex. For instance, [1] is true, whereas [] is false, and in general, classes can define their own notion of truthiness.

Ruby has "==", "===", ".eql", and ".equal". "===" is for case statements. "==" is related to ">=", etc. ".eql" tests that the two objects have the "same content". Usually, "==" and ".eql" return the same thing, but their implementation is different. ".equal" is reserved for object identity. Hence:
>> "a" == "a"
=> true
>> "a".eql?("a")
=> true
>> "a".equal?("a")
=> false
>> "a" === "a"
=> true
If you want to see all of a class's instance methods, use the "instance_methods" method. If you don't want to see inherited methods, pass false to "instance_methods":
>> class C
>> def f
>> end
>> end
=> nil
?> C.instance_methods
=> ["inspect", "f", "clone", "method", "public_methods",
"instance_variable_defined?", "equal?", "freeze", "methods", "respond_to?",
"dup", "instance_variables", "__id__", "object_id", "eql?", "require", "id",
"singleton_methods", "send", "taint", "frozen?", "instance_variable_get",
"__send__", "instance_of?", "to_a", "type", "protected_methods",
"instance_eval", "==", "display", "===", "instance_variable_set", "kind_of?",
"extend", "to_s", "hash", "class", "tainted?", "=~", "private_methods", "gem",
"nil?", "untaint", "is_a?"]
?> C.instance_methods(false)
=> ["f"]
Using Class.instance_methods.sort is helpful too.

When you are appending one string onto the end of another string, using += does not modify the original string, whereas << does:
>> s = "foo"
=> "foo"
>> t = s
=> "foo"
>> s += "bar"
=> "foobar"
>> s
=> "foobar"
>> t
=> "foo"
>> u = t
=> "foo"
>> u << "bar"
=> "foobar"
>> u
=> "foobar"
>> t
=> "foobar"
Indexing a string returns that character's ASCII value, which is a bit surprising. Slicing does what you would expect:
=> "abc"
>> s[1]
=> 98
>> s[1,1]
=> "b"
However, indexing a string to set a character works as expected:
>> s[1] = 'B'
=> "B"
>> s
=> "aBc"
Arrays have a "concat" method. "concat" and "+" operate as you might expect. "concat" is conceptually "concat!" because it modifies the array, but it's spelled without the "!":
>> s = [1, 2]
=> [1, 2]
>> t = [3, 4]
=> [3, 4]
>> s + t
=> [1, 2, 3, 4]
>> s
=> [1, 2]
>> s.concat(t)
=> [1, 2, 3, 4]
>> s
=> [1, 2, 3, 4]
>> t
=> [3, 4]
To get both the values and indexes of an array as you loop over it, use the each_with_index method:
>> ['a', 'b', 'c'].each_with_index {|x, i| puts "#{i} => #{x}"}
0 => a
1 => b
2 => c
There are a couple ways to create a hash. The second syntax is a bit weird to my eyes:
>> {:a => :b}
=> {:a=>:b}

>> Hash[:a => :b]
=> {:a=>:b}
By default, you'll get nil if you try to lookup a key that doesn't exist in a hash. As I've mentioned before, I prefer to get exceptions by default if I misspell things. Fortunately, the fetch method exists:
>> h = {:a => :b}
=> {:a=>:b}
>> h[:c]
=> nil
>> h.fetch(:c)
IndexError: key not found
from (irb):50:in `fetch'
from (irb):50
The default nil behavior of Ruby hashes is particular frustrating to me since Ruby uses hashes for keyword arguments. It's really easy for a misspelled option to lead to a silent bug:
>> def f(arg, options)
>> p arg
>> if options[:excited]
>> puts "Oh, yeah!" # This doesn't get printed.
>> end
>> end
=> nil
?> f("hello", :excitid => true) # Oops, typo.
=> nil
I think real support for keyword arguments is better. Here's the Python:
>>> def f(arg, excited=False):
... print arg
... if excited:
... print "Yeah!"
>>> f("hello", excitid=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'excitid'
You can still use the Ruby approach in Python using the "**kargs" syntax, but it's not the default.

I was a little surprised that you could connect two statements with a semicolon inside parenthesis:
>> (puts 'hi'; puts 'there') unless 100 < 10
=> nil
Iterating over a string yields lines, not characters like it does in Python:
>> "foo\nbar".each {|line| puts line}
=> "foo\nbar"
When using regular expressions, the =~ operator returns the index of the match, whereas the #match method returns a MatchData object:
>> /o/ =~ 'foo'
=> 1
>> /o/.match('foo')
=> #<MatchData:0x35d5cc>
Be careful. The MatchData object stores captures starting at 1, whereas the #captures method stores captures starting at 0:
>> match = /(o)/.match('foo')
=> #<MatchData:0x357654>
>> match[1]
=> "o"
>> match.captures[0]
=> "o"
See my earlier post concerning "^" vs. "\A" in regular expressions. Ruby and Python are subtly different. Whereas I would normally use "^" in Python, to get the same behavior, I would use "\A" in Ruby.

If you want to treat a string as an array of characters, use '"foobar".split(//)'.

String#scan is a pretty cool method that repeatedly tries to match a regular expression while iterating over a string. Unlike #match, it returns all of the matches, instead of just the first:
>> s = "John Doe was the father of Jane Doe."
=> "John Doe was the father of Jane Doe."
>> s.scan(/([A-Z]\w+)\s+([A-Z]\w+)/)
=> [["John", "Doe"], ["Jane", "Doe"]]
Okay, that's it for this post, but I've got more coming :)


Philip Jenvey said…
Note that Python's re.findall is analogous to String#scan
jjinux said…
Phil, nice! I didn't know about that.
jjinux said…
The next post in the series is here:

Popular posts from this blog

Ubuntu 20.04 on a 2015 15" MacBook Pro

I decided to give Ubuntu 20.04 a try on my 2015 15" MacBook Pro. I didn't actually install it; I just live booted from a USB thumb drive which was enough to try out everything I wanted. In summary, it's not perfect, and issues with my camera would prevent me from switching, but given the right hardware, I think it's a really viable option. The first thing I wanted to try was what would happen if I plugged in a non-HiDPI screen given that my laptop has a HiDPI screen. Without sub-pixel scaling, whatever scale rate I picked for one screen would apply to the other. However, once I turned on sub-pixel scaling, I was able to pick different scale rates for the internal and external displays. That looked ok. I tried plugging in and unplugging multiple times, and it didn't crash. I doubt it'd work with my Thunderbolt display at work, but it worked fine for my HDMI displays at home. I even plugged it into my TV, and it stuck to the 100% scaling I picked for the othe

Drawing Sierpinski's Triangle in Minecraft Using Python

In his keynote at PyCon, Eben Upton, the Executive Director of the Rasberry Pi Foundation, mentioned that not only has Minecraft been ported to the Rasberry Pi, but you can even control it with Python . Since four of my kids are avid Minecraft fans, I figured this might be a good time to teach them to program using Python. So I started yesterday with the goal of programming something cool for Minecraft and then showing it off at the San Francisco Python Meetup in the evening. The first problem that I faced was that I didn't have a Rasberry Pi. You can't hack Minecraft by just installing the Minecraft client. Speaking of which, I didn't have the Minecraft client installed either ;) My kids always play it on their Nexus 7s. I found an open source Minecraft server called Bukkit that "provides the means to extend the popular Minecraft multiplayer server." Then I found a plugin called RaspberryJuice that implements a subset of the Minecraft Pi modding API for B

Creating Windows 10 Boot Media for a Lenovo Thinkpad T410 Using Only a Mac and a Linux Machine

TL;DR: Giovanni and I struggled trying to get Windows 10 installed on the Lenovo Thinkpad T410. We struggled a lot trying to create the installation media because we only had a Mac and a Linux machine to work with. Everytime we tried to boot the USB thumb drive, it just showed us a blinking cursor. At the end, we finally realized that Windows 10 wasn't supported on this laptop :-/ I've heard that it took Thomas Edison 100 tries to figure out the right material to use as a lightbulb filament. Well, I'm no Thomas Edison, but I thought it might be noteworthy to document our attempts at getting it to boot off a USB thumb drive: Download the ISO. Attempt 1: Use Etcher. Etcher says it doesn't work for Windows. Attempt 2: Use Boot Camp Assistant. It doesn't have that feature anymore. Attempt 3: Use Disk Utility on a Mac. Erase a USB thumb drive: Format: ExFAT Scheme: GUID Partition Map Mount the ISO. Copy everything from