Skip to content

Method lookup walks ancestors; super continues it

Level: 301 · a reader who wants to predict which method runs, and what super with no parentheses passes

One line: A call searches the receiver's ancestors from left to right and stops at the first class or module that defines the name; super resumes that same walk from the current entry, and a bare super passes the current values of the caller's parameters while super() passes nothing.

Every object's class has an ancestors list: prepended modules, the class itself, included modules (the one included last comes first), then the superclass with its modules, and so on up to Object, Kernel and BasicObject. A method call is a search of that list, and super is the same search continued from the entry where the current method was found — which is why a module's super reaches the class's superclass, and why a prepended module's super reaches the class itself. super has two spellings that beginners conflate: with no parentheses (zsuper) it forwards the caller's parameters as they are now, reassignments included; super() forwards nothing, so the parent's defaults apply. Method#owner says where a method was found and Method#super_method is super as an object, so the walk can be printed. When the walk reaches BasicObject without a match, Ruby calls method_missing, whose default raises NoMethodError. The program builds Animal, Dog with an included and a prepended module, and a diamond of modules, and measures each of these.

Verified output of method_lookup_and_super_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. Dog.ancestors                                              [Wrapped, Dog, Walkable, Animal, Object, Kernel, BasicObject]
 2. the diamond (B and C include A) -- D.ancestors.take(4)     [D, C, B, A]
 3. zsuper -- Dog.new.speak(3)                                 "[dog:animal x3]"
 4. super() -- Cat.new.speak(3)                                "cat:animal x1"
 5. zsuper after times += 1 -- Dog.new.shout(3)                "ANIMAL x4"
 6. super inside a module method -- dog.move                   "walk, then animal moves"
 7. initialize without super / with super -- instance_variables [:@sub] / [:@base, :@sub]
 8. method(:speak).owner, then super_method, until nil         [Wrapped, Dog, Animal]
 9. super with no superclass method -- Cat.new.purr            NoMethodError: super: no superclass method 'purr' for an instance of Cat
10. ends in method_missing -- dog.fly / respond_to?(:fly)      "method_missing caught fly" / false
11. Dog.superclass / BasicObject.superclass / method(:puts).owner Animal / nil / Kernel

Reading the output

Row 1 is the whole chain for Dog: the prepended Wrapped, Dog, the included Walkable, the superclass Animal, then Object, Kernel and BasicObject. Row 2 is the diamond — B and C both include A, D includes B and then C — and shows two rules at once: a module appears once however many paths lead to it, and the module included last is searched first, so the chain reads [D, C, B, A].

Rows 3 to 6 are super. Dog#speak(3) calls bare super, so Animal#speak receives 3 — and because Wrapped is prepended, its speak ran first and wrapped the whole result in brackets (row 3). Cat#speak(3) calls super(), so Animal#speak receives nothing and uses its default of 1 (row 4). Row 5 is the subtlety of zsuper: Dog#shout does times += 1 before super, and the parent receives 4, the parameter's current value. Row 6 is super inside a module method: Walkable#move continues the walk from Walkable's slot and lands on Animal#move.

Row 7 is the constructor version of the same rule: initialize is an ordinary method, so a subclass's initialize that never calls super silently skips the parent's, and @base is never set. Row 8 prints the walk as objects: method(:speak).owner is Wrapped, its super_method is owned by Dog, the next by Animal, and the next is nil. Row 9 is what happens when super finds nothing: a NoMethodError whose message starts with super:. Row 10 is the end of the walk — dog.fly is answered by method_missing, and respond_to?(:fly) stays false because nothing told Ruby to say otherwise. Row 11 separates superclass (classes only: Dog.superclass is Animal, and BasicObject's is nil) from ancestors (classes and modules), and shows that puts is found in Kernel, a module in every chain.

Compared with Python

Python's chain is __mro__, computed by the C3 linearization from the order of the bases, and rows 1 and 2 print it. Two things differ from Ruby's list. There is no prepend, so a class always comes first in its own MRO (row 1 starts with Dog, not a wrapper). And the diamond class D(B, C) linearizes to ['D', 'B', 'C', 'A'] — the base listed first is searched first, the opposite of Ruby, where the module included last is. super() in Python takes explicit arguments: row 3 writes super().speak(times), row 4 super().speak(), and row 5's reassignment of times reaches the parent for the obvious reason, not because of a zsuper rule. Row 6 shows that a mixin's super() is cooperative — it goes to the next class in the MRO, Animal — and row 7 that super().__init__() is as easy to forget in Python as super is in Ruby, with the same result: the parent's attribute is never set. Row 9's AttributeError is the counterpart of row 9's NoMethodError, and row 10 is the one place the twin diverges in behaviour: __getattr__ runs when normal lookup fails, like method_missing, but hasattr sees its answer, so it prints True where Ruby's respond_to? printed false. Row 11: there is no Kernel in the chain, because print is not a method at all — it lives in the builtins module.

Verified output of method_lookup_and_super_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. Dog.__mro__                                                ['Dog', 'Walkable', 'Animal', 'object']
 2. the diamond (B and C extend A) -- D.__mro__[:4]            ['D', 'B', 'C', 'A']
 3. super().speak(times) -- Dog().speak(3)                     'dog:animal x3'
 4. super().speak() -- Cat().speak(3)                          'cat:animal x1'
 5. super().shout(times) after times += 1 -- Dog().shout(3)    'ANIMAL x4'
 6. super() inside a mixin method -- dog.move()                'walk, then animal moves'
 7. __init__ without super() / with super() -- vars()          ['sub'] / ['base', 'sub']
 8. classes in Dog.__mro__ that define speak                   ['Dog', 'Animal']
 9. super().purr() with no base method -- Cat().purr()         AttributeError
10. ends in __getattr__ -- dog.fly / hasattr(dog, "fly")       '__getattr__ caught fly' / True
11. Dog.__bases__ / object.__bases__ / where print lives       ['Walkable', 'Animal'] / () / builtins

The linearization is explained in The Python 2.3 Method Resolution Order ↗, and super()'s two-argument form in super ↗.

Try it

  1. Build the diamond with modules — B and C both include A, D includes B then C — give every module a name method that calls super and prepends its own letter, and print D.ancestors up to Object beside D.new.name, so the walk is visible in the string.
  2. Add respond_to_missing? to Dog so that respond_to?(:fly) becomes true and dog.method(:fly) works, then print the same two rows again. (method_missing and respond_to_missing? measures the before and after.)
Solution to 1

method_lookup_and_super_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: the diamond with modules -- print the chain, then let each
# `name` call super and append itself, so the walk is visible in the result.

module A
  def name = "A"
end

module B
  include A
  def name = "B > " + super
end

module C
  include A
  def name = "C > " + super
end

class D
  include B
  include C
  def name = "D > " + super
end

puts "D.ancestors up to Object: #{D.ancestors.take_while { |m| m != Object }.inspect}"
puts "D.new.name:               #{D.new.name}"
puts "A appears once, and the module included last (C) is reached first"

Verified output of method_lookup_and_super_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

D.ancestors up to Object: [D, C, B, A]
D.new.name:               D > C > B > A
A appears once, and the module included last (C) is reached first

See also