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¶
- Build the diamond with modules —
BandCboth includeA,DincludesBthenC— give every module anamemethod that callssuperand prepends its own letter, and printD.ancestorsup toObjectbesideD.new.name, so the walk is visible in the string. - Add
respond_to_missing?toDogso thatrespond_to?(:fly)becomestrueanddog.method(:fly)works, then print the same two rows again. (method_missingandrespond_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.
See also¶
- Mixins:
include,extend,prepend— how each entry got into the chain - Classes are open — reopening a class changes the entry, not the chain
privatemeans "no receiver" — the visibility check that runs once the walk has found the methodmethod_missingandrespond_to_missing?— what to do at the end of the walk- Method objects and unbound methods —
owner,super_methodandbindas a whole lesson BasicObjectandKernel— the last two entries of every chain- Supertraits ↗ — Rust has no method walk at all: a trait names what it requires, and the compiler checks it
- Constants are found lexically, then through ancestors — A bare constant name is resolved by the lexical nesting the code was written in (
Module.nesting), then… - Hooks fire when code is defined, not when it runs —
inherited,included,extended,prepended,method_added,singleton_method_addedand… - An object can list its methods and variables —
methods,instance_methods(false),instance_variables,instance_method(:x).parameters,…