send reaches private methods; public_send does not¶
Level: 301 · for a reader who knows what private means in Ruby and wants to call a method whose name is computed at run time
One line: obj.send(:name, *args) calls a method by name and ignores visibility, public_send honours it, and __send__ still works on an object whose class has its own send — while Python's getattr(obj, "name")(*args) has no visibility to ignore, only a naming convention and a compile-time mangling rule.
A Ruby method call is a message: calc.add(2, 3) sends :add with two arguments to calc. send lets you compose that message from data — a Symbol, a String, or a name built by interpolation — which is what every serializer, test framework and ORM does. It is defined on Kernel, and it has one property that surprises people: it ignores private and protected. That is deliberate (tests and libraries use it to reach internals), and it is why Ruby 1.9 added public_send, which dispatches the same way but refuses a private method exactly as an explicit receiver would (the history is not machine-checked here). The third spelling, BasicObject#__send__, exists because send is an ordinary method name that a class may take for itself — a Mailer or a socket wants a send — and the underscored name is the one nobody overrides. The program measures all three, plus respond_to?, the question to ask before sending a name that came from outside.
Verified output of send_and_public_send_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. calc.send(:add, 2, 3) 5
2. calc.send("add", 2, 3) -- a String works too 5
3. Report.new.send(:"to_#{fmt}"), fmt = "csv" "a,b"
4. 1.send(:+, 2) -- operators are methods 3
5. [1, 2, 3].send(:map) { it * 2 } -- block passes [2, 4, 6]
6. vault.send(:code) -- a reader is a method call 42
7. vault.send(:secret) -- private, still called "the code is 42"
8. vault.public_send(:secret) NoMethodError: private method 'secret' called for an instance of Vault
9. respond_to?(:secret) / respond_to?(:secret, true) false / true
10. vault.send(:nope) -> e.class, e.name NoMethodError, :nope
11. mailer.send(:deliver) -- send is overridden "mailing :deliver"
mailer.__send__(:deliver) "delivered"
mailer.public_send(:deliver) "delivered"
12. owner of send / __send__ / public_send Kernel / BasicObject / Kernel
13. calc.send(:add, 1) -- arity is still checked ArgumentError: wrong number of arguments (given 1, expected 2)
Reading the output¶
Rows 1–5 are the ordinary uses: the name may be a Symbol or a String, it may be assembled at run time (:"to_#{fmt}"), an operator is just a method with a strange name, and a block passes through send like any other call. Row 6 shows there is no separate notion of "attribute" to fetch — attr_reader :code defined a method, so send(:code) is a call. Rows 7 and 8 are the lesson: send calls the private secret as if it were public, while public_send raises the same NoMethodError that vault.secret would, with the same "private method 'secret' called for an instance of Vault" message. Row 9 is the polite way to find out in advance: respond_to? answers false for a private method unless you pass true as its second argument. Row 10 shows what an unknown name costs — a NoMethodError whose name attribute carries the Symbol you sent — because send has no default-value form. Row 11 is why __send__ exists: Mailer defines its own send, so mailer.send(:deliver) runs the mailer's method with :deliver as its message argument; __send__ and public_send still dispatch. Row 12 locates the three methods: send and public_send live in Kernel, __send__ in BasicObject, so even a BasicObject proxy has __send__. Row 13: send relaxes visibility and nothing else — the arity check still fires.
Compared with Python¶
The twin uses getattr, which does not call anything: it returns the attribute, and you call the result, so getattr(vault, "_code") hands back the integer and callable says False (row 6) where Ruby's send(:code) ran a method. Python has no private: one leading underscore is a convention that getattr ignores as readily as send ignores private (row 7). Two leading underscores are mangled at compile time to _Vault__mangled, so getattr(vault, "__mangled") fails not because access is forbidden but because no attribute has that name, and the mangled name works (row 8) — the only wall Python builds is a naming wall, which is why there is no public_send. hasattr is respond_to?, and it simply tries the lookup (row 9). getattr takes a default where Ruby makes you rescue or ask first (row 10). Because getattr is a builtin function and not a method on the object, a class cannot shadow it: the Mailer twin's own getattr method is just another method, and nothing plays the role of __send__ (row 11). operator.methodcaller (row 2) packages a name and arguments for later, the way a Ruby lambda around send would, and operator.add is the function form of + (row 4). Row 13 is the same in both: dispatching by name does not relax the argument count.
Verified output of send_and_public_send_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. getattr(calc, "add")(2, 3) 5
2. operator.methodcaller("add", 2, 3)(calc) 5
3. getattr(Report(), f"to_{fmt}")(), fmt = "csv" 'a,b'
4. (1).__add__(2) / operator.add(1, 2) 3 / 3
5. list(map(lambda x: x * 2, [1, 2, 3])) [2, 4, 6]
6. getattr(vault, "_code") / callable(...) 42 / False
7. getattr(vault, "_secret")() -- convention only 'the code is 42'
8. getattr(vault, "__mangled") AttributeError: no attribute '__mangled'
getattr(vault, "_Vault__mangled") 7
9. hasattr: "_secret" / "__mangled" / "_Vault__mangled" True / False / True
10. getattr(vault, "nope") -> type, e.name AttributeError, 'nope'
getattr(vault, "nope", None) -- a default None
11. getattr(mailer, "deliver")() -- a builtin, not a method 'delivered'
mailer.getattr("deliver") -- the class's own method 'shadow'
no __send__ needed getattr cannot be shadowed by the class
12. getattr.__module__ / type(getattr).__name__ builtins / builtin_function_or_method
13. getattr(calc, "add")(1) -- arity is still checked TypeError
Try it¶
- Write
dispatch(obj, name, *args)that returns:unknownunlessobj.respond_to?(name)and otherwise callsobj.public_send(name, *args). Try it onCalc#add, on a privateVault#secret, on a misspelt name, and once with a name built byformat. - Give a socket-like class its own
send(bytes)method and show three ways to still call another of its methods by a run-time name:__send__,public_sendandmethod(name).call. Which of the three still refuses a private method, and what doesrespond_to?need in order to saytruefor it?
Solution to 1
send_and_public_send_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# send_and_public_send_kata_rb.rb — dispatch(obj, name, *args): ask before sending.
class Calc
def add(a, b) = a + b
end
class Vault
private
def secret = "the code"
end
def dispatch(obj, name, *args)
return :unknown unless obj.respond_to?(name) # private methods answer false here
obj.public_send(name, *args)
end
puts "1. dispatch(Calc.new, :add, 2, 3) -> #{dispatch(Calc.new, :add, 2, 3).inspect}"
puts "2. dispatch(Vault.new, :secret) -> #{dispatch(Vault.new, :secret).inspect}"
puts "3. dispatch(Calc.new, :addd, 2, 3) -> #{dispatch(Calc.new, :addd, 2, 3).inspect}"
op = "add"
puts "4. dispatch(Calc.new, format(\"%s\", op), 5, 6) -> #{dispatch(Calc.new, format("%s", op), 5, 6).inspect}"
Verified output of send_and_public_send_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
method_missingneedsrespond_to_missing?— what happens when the namesendcarries matches no methoddefine_methodturns a block into a method — the other half: making the method whose name you computed- An object can list its methods and variables —
respond_to?,methodsandmethod_defined?, the questions to ask beforesend - Private means no receiver — the rule
sendbypasses andpublic_sendkeeps - Method objects and unbound methods —
method(:add).call, the reified alternative tosend - Everything is an object — why
1.send(:+, 2)works - Common error messages — the
NoMethodErrorwordings rows 8 and 10 print - ABAP: dynamic programming — names decided at runtime ↗ — the same idea in ABAP, where a method name in a string moves every check to run time
&:symcallsSymbol#to_proc—&xin an argument list callsx.to_procand attaches the result as the block; a symbol'sto_procis a…newisallocatetheninitialize—Point.new(1, 2)is a class method that does two things- Refinements are lexical, not global —
refine String do … endinside a module builds aRefinement, andusingthat module activates it from…