There is more than one way, and one obvious way¶
Level: 101 · a Python programmer's first Ruby afternoon, and the other way round
One line: Ruby's idioms — each, blocks, ? and ! methods, unless, attr_accessor, nil, Symbol, mixins, Comparable, Enumerable, method_missing, Struct, Data, case/in, Rational — each have a Python twin, printed here on one line each; Ruby keeps several names for one operation where Python keeps one, and import this has no Ruby counterpart at all.
Perl's motto was "there is more than one way to do it", and Ruby inherited the attitude: map and collect give the same result, so do select and filter, inject and reduce, size and length, then and yield_self. Python's answer, in the Zen it ships as a module, is that "there should be one — and preferably only one — obvious way to do it", and the language mostly keeps that promise: a list has sort and nothing else spelled like it. Neither attitude makes the other language's idioms unavailable; it decides which spelling the community reads as native. The table below is the crosswalk this library keeps coming back to, one lesson per row; the program prints one measured line per row, then checks the aliases, then asks Ruby for its Zen (there is none — require "this" raises), and ends with a five-line word-frequency program whose output the Python twin reproduces to the byte.
| # | The Ruby way | The Pythonic way | Lesson |
|---|---|---|---|
| 1 | each with a block |
for |
for loops do not scope |
| 2 | select/map blocks |
a comprehension | Blocks are not objects |
| 3 | empty? — a ? predicate |
not xs, is_/isdigit-style names |
Bang and question methods |
| 4 | sort! returns the receiver |
sort() returns None |
Bang and question methods |
| 5 | unless |
if not |
unless, until and modifiers |
| 6 | attr_accessor |
a plain attribute, or @property |
Instance variables are private |
| 7 | nil |
None |
nil, false and truthiness |
| 8 | puts |
print |
puts, print, p and pp |
| 9 | require |
import |
require, require_relative and load |
| 10 | Hash |
dict |
Hashes and default values |
| 11 | Symbol |
str, interned, or an Enum |
Symbols are names |
| 12 | module as a mixin |
a mixin base class | Mixins: include, extend, prepend |
| 13 | Comparable + <=> |
functools.total_ordering + __lt__/__eq__ |
Comparable and the spaceship |
| 14 | Enumerable + each |
the iterator protocol, __iter__ |
Enumerable is a mixin |
| 15 | method_missing |
__getattr__ |
method_missing and respond_to_missing? |
| 16 | Struct |
namedtuple |
Struct and Data |
| 17 | Data |
@dataclass(frozen=True) |
Struct and Data |
| 18 | case … in |
match … case |
case/in pattern matching |
| 19 | Rational, 1/3r |
fractions.Fraction |
Rational and BigDecimal |
Verified output of the_ruby_way_and_the_pythonic_way_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
The idiom pairs
1. [1, 2, 3].each { |x| out << x.to_s } "123"
2. xs.select(&:even?).map { it * it } [4, 16]
3. [].empty? / [1].empty? true / false
4. a = [3, 1]; a.sort! returns [1, 3] (the receiver, now sorted)
5. unless [].any? then "empty" end "empty"
6. attr_accessor :name; person.name = "ann" "ann"
7. nil.class / nil.inspect / nil.to_s NilClass / nil / ""
8. r = puts "hi" (printed "hi\n") r = nil
9. require "json" twice true / false (loaded once)
10. {a: 1}[:a] / [:z] / fetch(:z, 0) 1 / nil / 0
11. :a.equal?(:a) / "a".equal?("a") true / false
12. include Greeting; Host.new.hello "hello from the mixin" ancestors [Host, Greeting, Object]
13. include Comparable, <=> only: V(1) < V(2) true between? true
14. include Enumerable, each only: sort / map [1, 2] / [20, 10]
15. method_missing: Finder.new.find_by_name("x") "name=x"
16. Struct.new(:x, :y).new(1, 2) #<struct Point x=1, y=2>
17. Data.define(:x, :y): with(y: 5) / set @x #<data Coord x=1, y=5> / FrozenError
18. case/in: shape([1, 2]) / shape({name: "ann"}) "pair 1,2" / "named ann"
19. 1/3r + 1/6r (1/2)
More than one way: two names, one result
collect==map true, filter==select true, reduce==inject true, size==length true, yield_self==then true
Method#== (one definition under two names): then/yield_self true, map/collect false (two definitions of one behaviour)
The Zen: Ruby ships no equivalent of `import this`
require "this" -> LoadError: cannot load such file -- this
Word frequency, the same program in both languages
the 4
and 2
barks 2
dog 2
fox 2
Reading the output¶
- Rows 1–5 are the syntax habits: iteration with a block, filtering and mapping with blocks, a
?predicate, a!method that mutates and hands back its receiver, andunlessas an expression with a value. - Rows 6–11 are the vocabulary: generated accessors,
niland its two string forms,putsreturningnilafter printing,requirereturningtruethe first time andfalsethe second (the file loads once),Hashlookups withnilfor a missing key, and the identity that makes aSymboldifferent from aString. - Rows 12–15 are the object model: a module mixed in appears in
ancestors;Comparablederives five operators andbetween?from<=>;Enumerablederivessortandmapfromeach;method_missinganswers a name that was never defined. - Rows 16–19: the value types — a mutable
Struct, an immutableData(withreturns a copy; setting an ivar is aFrozenError), pattern matching on an array and a hash, and exact rational arithmetic. - Two names, one result: five pairs of spellings, five equal results. The second line is a finer point:
Method#==saysthenandyield_selfare one definition under two names (a true alias), whilemapandcollectare two separate definitions of the same behaviour — Ruby offers the second name either way. - The Zen:
require "this"is aLoadError. Ruby's design philosophy lives in Matz's talks and books, not in a module. (Not machine-checked here.) - Word frequency:
scan,tally,sort_bywith a two-element key,first(5),format— five lines.
Compared with Python¶
The twin prints the same nineteen rows. Rows 4 and 8 are where a Python reader most often stumbles in Ruby: Python's sort() returns None and mutates, while Ruby's sort! returns the sorted receiver so it can be chained; print and puts at least agree on returning nothing. Row 6 needs no declaration — attributes are public by default. Row 9 shows import returning a module object and caching it in sys.modules, the counterpart of require's true/false. Row 11 has no symbol type: sys.intern gives the identity, an Enum gives the fixed vocabulary. Rows 12–15 are the same four ideas under Python's names — a base class in the MRO, total_ordering, __iter__, __getattr__. Rows 16–19 use namedtuple, a frozen dataclass (FrozenInstanceError on assignment), match, and Fraction. The one-way check finds none of Ruby's alias names on a list, the Zen prints its twenty-one lines, and the word-frequency program — Counter, re.findall, sorted with a tuple key — prints the same five lines the Ruby program did.
Verified output of the_ruby_way_and_the_pythonic_way_py.py — regenerated by tools/run_examples.py, never hand-typed.
The idiom pairs
1. for x in [1, 2, 3]: out += str(x) '123'
2. [x * x for x in xs if x % 2 == 0] [4, 16]
3. not [] / not [1] True / False
4. a = [3, 1]; a.sort() returns None (a is now [1, 3])
5. if not any([]): "empty" 'empty'
6. person.name = "ann" (no declaration) 'ann'
7. type(None).__name__ / repr(None) / str(None) NoneType / None / 'None'
8. r = print("hi") (printed 'hi\n') r = None
9. import json twice module / True (loaded once, cached in sys.modules)
10. {"a": 1}["a"] / .get("z") / .get("z", 0) 1 / None / 0
11. sys.intern("a") is sys.intern("a") / Key.A True / <Key.A: 'a'>
12. class Host(Greeting): Host().hello() 'hello from the mixin' mro ['Host', 'Greeting', 'object']
13. @total_ordering, __lt__ and __eq__: V(1) < V(2) True <= True
14. __iter__ only: sorted(bag) / map [1, 2] / [20, 10]
15. __getattr__: Finder().find_by_name("x") 'name=x'
16. namedtuple("Point", "x y")(1, 2) Point(x=1, y=2)
17. frozen dataclass: replace(y=5) / set .x Coord(x=1, y=5) / FrozenInstanceError
18. match: shape([1, 2]) / shape({"name": "ann"}) 'pair 1,2' / 'named ann'
19. Fraction(1, 3) + Fraction(1, 6) Fraction(1, 2)
One obvious way: the aliases Ruby has do not exist
hasattr([], 'collect') False, hasattr([], 'filter') False, hasattr([], 'length') False, hasattr([], 'size') False, hasattr(5, 'then') False
The Zen, as `import this` prints it
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
Word frequency, the same program in both languages
the 4
and 2
barks 2
dog 2
fox 2
The Python library's crosswalk ↗ and glossary ↗ are the same table seen from the other side.
Try it¶
- Prove "more than one way" on the word-frequency program: write it three ways in Ruby — with
tally, witheach_with_object(Hash.new(0)), and withgroup_by(&:itself)— print all three top-five lists, and print whether the three results are equal. - Pick five rows of the table, write the Python side from memory into a file, run it under
python3 -I, and diff your lines against the twin's output above.
Solution to 1
the_ruby_way_and_the_pythonic_way_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: "more than one way" made concrete - the word-frequency program written
# three ways, with a check that all three agree.
TEXT = "the quick brown fox jumps over the lazy dog the fox sleeps and the dog barks and barks"
def top_five_with_tally(text)
text.scan(/[a-z']+/).tally.sort_by { |w, c| [-c, w] }.first(5)
end
def top_five_with_each_with_object(text)
counts = text.scan(/[a-z']+/).each_with_object(Hash.new(0)) { |w, h| h[w] += 1 }
counts.sort_by { |w, c| [-c, w] }.first(5)
end
def top_five_with_group_by(text)
text.scan(/[a-z']+/).group_by(&:itself).map { |w, ws| [w, ws.size] }.sort_by { |w, c| [-c, w] }.first(5)
end
results = {
"tally" => top_five_with_tally(TEXT),
"each_with_object" => top_five_with_each_with_object(TEXT),
"group_by" => top_five_with_group_by(TEXT),
}
results.each do |name, pairs|
puts "#{name}:"
pairs.each { |w, c| puts format(" %-8s %d", w, c) }
end
puts "all three equal? #{results.values.uniq.size == 1}"
Verified output of the_ruby_way_and_the_pythonic_way_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Twenty-three Ruby traps for a Python programmer — the rows above, where they go wrong
- Twenty-three Python traps for a Ruby programmer — the same crossing in the other direction
tapreturns the receiver,thenreturns the block's value — one more pair of aliases (then/yield_self) and the chaining style around them- The enumerable toolbox —
tally,sort_byand the rest of the word-frequency program - Resources — where Ruby's design philosophy is written down
- PEP 20 ↗ — the Zen of Python, as a document