An object can list its methods and variables¶
Level: 301 · for a reader debugging a library object in irb and wanting to ask it, rather than the docs, what it can do
One line: methods, instance_methods(false), instance_variables, instance_method(:x).parameters, source_location, ancestors, constants, singleton_methods, local_variables, defined? and __method__ answer at run time what a Python programmer asks with dir, vars, inspect.signature, inspect.getsourcefile, __mro__, locals() and sys._getframe() — with one structural difference: Ruby's attr_reader values are methods, so every listing separates methods from variables, while Python's dir mixes data attributes and functions.
Ruby keeps every class, method and variable as an object you can question. obj.methods lists the public methods an object answers to (subtracting Object.instance_methods leaves the interesting ones); Klass.instance_methods(false) lists only what the class itself defined, and private_instance_methods(false) the private ones, initialize included. instance_variables is the list of @names set so far, and instance_variable_get reads one without a reader. A method is an object too: instance_method(:name) gives an UnboundMethod with parameters — every parameter with its kind, :req, :opt, :rest, :keyreq, :key, :keyrest, :block — an arity, an owner and a source_location. ancestors is the lookup chain, constants the names a module holds, singleton_methods the methods one object alone has. Inside a method, local_variables and __method__ describe the frame, and defined? is a keyword that reports what kind of thing a name is without evaluating it: "local-variable", "method", "constant", "instance-variable", "expression" — or nil. The program asks each question of a small Geometry::Point.
Verified output of introspection_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. point.methods - Object.instance_methods (sorted) [:<, :<=, :>, :>=, :area, :between?, :clamp, :configure, :move!, :x, :y]
2. Point.instance_methods(false).sort [:<=>, :area, :configure, :move!, :x, :y]
Point.private_instance_methods(false).sort [:initialize, :secret]
3. point.instance_variables [:@x, :@y]
instance_variable_get(:@x) for each {"@x": 1, "@y": 2}
4. instance_method(:configure).parameters [[:req, :a], [:opt, :b], [:rest, :r], [:keyreq, :k], [:key, :d], [:keyrest, :o], [:block, :blk]]
arity of configure / area / move! -3 / 0 / 2
5. instance_method(:area).source_location introspection_rb.rb:20
6. Point.ancestors [Geometry::Point, Comparable, Object, Kernel, BasicObject]
7. Geometry.constants.sort [:ORIGIN, :Point]
Geometry.const_source_location(:ORIGIN) introspection_rb.rb:12
8. Point.singleton_methods [:origin]
after define_singleton_method: point.singleton_methods [:label]
9. local_variables inside a method (a = 1; b = 2) [:a, :b]
10. defined?(a) local / String / Nope / puts / nope ["local-variable", "constant", nil, "method", nil]
defined?(@x) / (a = 1) / 3 / self / $stdout ["instance-variable", "assignment", "expression", "self", "global-variable"]
defined?(yield) / (super) with no block, no parent [nil, nil]
11. __method__ inside whoami :whoami
12. method(:area).owner / method(:between?).owner Geometry::Point / Comparable
method_defined?(:area) / (:secret) / private_method_defined?(:secret) true / false / true
instance_variable_defined?(:@x) / (:@z) true / false
13. point.class / Point.class / Geometry.class Geometry::Point / Class / Module
Point.name / Point.singleton_class "Geometry::Point" / #<Class:Geometry::Point>
Reading the output¶
Row 1 subtracts Object's methods and leaves what Point and Comparable added — note that x and y are there, because attr_reader defines methods, and so are <, between? and clamp, which Comparable derived from <=>. Row 2 narrows to what the class body itself defined, public and then private; initialize is always private. Row 3 lists the variables as @-prefixed Symbols and reads each one. Row 4 is the richest answer: parameters names every parameter with its kind, and arity summarises them (a negative arity means optional parameters; -3 here counts a and the required keyword k as two mandatory arguments). Row 5 gives the file and line of a method's definition — the basename is printed because the full path would differ between machines. Row 6 is the lookup chain the method lookup lesson walks. Row 7: constants lists both the constant and the nested class, and const_source_location says where the constant was assigned. Row 8: class methods are the class's singleton methods, and a method added to one object shows only on that object. Rows 9–11 look inward from inside a method: local_variables lists the frame's names, defined? classifies a local, a constant, a method call, a missing name, an instance variable, an assignment, a literal, self and a global, and answers nil for yield with no block and for super with no parent method; __method__ is the running method's name. Row 12: owner says which class or module a method came from, method_defined? sees public and protected methods only, and private_method_defined? the private ones. Row 13: an instance's class, a class's class (Class), a module's class (Module), a class's full name, and its singleton class, which prints as #<Class:Geometry::Point>.
Compared with Python¶
dir(point) (row 1) lists attributes and methods together, and x, y are instance attributes there, not methods; Ruby's list has only methods because Ruby has only methods — inspect.getmembers(point, inspect.ismethod) is the filter that gives back a methods-only list — bound dunder methods such as __init__ included, the _secret convention ignored, and the bound classmethod origin counted as a method. Python's "own methods of the class" is vars(Point) filtered with inspect.isfunction (row 2), and it lists _secret like the rest — a private method is a naming convention and no listing hides it. vars(point) is instance_variables and their values in one dictionary (row 3). inspect.signature (row 4) is parameters and arity together, printed as a signature string, and each parameter's kind — POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, KEYWORD_ONLY, VAR_KEYWORD — is the twin of Ruby's :req/:opt/:rest/:keyreq/:key/:keyrest; there is no :block kind because a callback is an ordinary parameter. inspect.getsourcefile and getsourcelines(...)[1] are source_location, and inspect.getsource goes one step further than Ruby can and returns the text of the definition (row 5), __mro__ is ancestors (row 6), and a namespace's names come from vars() again (row 7), since Python has no constants and no constants — ORIGIN is a class attribute that happens to be upper-case. Class methods are classmethod objects in the class dictionary, and a per-object method is a bound types.MethodType stored in the instance dictionary (row 8). locals() inside a method includes self (row 9) — and is read-only in spirit: writing into it has never reliably changed a variable, and Python 3.13 made that official (not machine-checked here). Row 10: there is no defined?; Python answers piecemeal with in locals(), hasattr, callable and a try/except NameError, and an assignment is a statement with no value to classify. sys._getframe().f_code.co_name is __method__ (row 11), __qualname__ and inspect.ismethod cover owner and method_defined? (row 12), and type() applied three times (row 13) shows that in Python a class, a namespace class and the metaclass are all instances of type, where Ruby distinguishes Class from Module.
Verified output of introspection_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. [n for n in dir(point) if not n.startswith('_')] ['area', 'configure', 'move', 'origin', 'x', 'y']
inspect.getmembers(point, inspect.ismethod) names ['__init__', '__lt__', '_secret', 'area', 'configure', 'move', 'origin']
2. own functions of Point (vars, minus dunders) ['_secret', 'area', 'configure', 'move']
_secret is listed like any other one underscore is a naming convention
3. vars(point) {'x': 1, 'y': 2}
getattr(point, name) for each {'x': 1, 'y': 2}
4. inspect.signature(Point.configure) (self, a, b=1, *r, k, d=2, **o)
parameter kinds self:POSITIONAL_OR_KEYWORD a:POSITIONAL_OR_KEYWORD b:POSITIONAL_OR_KEYWORD r:VAR_POSITIONAL k:KEYWORD_ONLY d:KEYWORD_ONLY o:VAR_KEYWORD
5. inspect.getsourcefile / getsourcelines(area)[1] introspection_py.py:27
inspect.getsource(area).splitlines()[0] def area(self):
6. [c.__name__ for c in Point.__mro__] ['Point', 'object']
7. own names of Geometry ['ORIGIN', 'Point']
inspect.getsourcelines(Geometry)[1] 15
8. classmethods of Point ['origin']
callables in vars(point) after MethodType ['label']
9. sorted(locals()) inside a method (a = 1; b = 2) ['a', 'b', 'self']
10. 'a' in locals() / str / Nope / print / nope [True, True, None, True, None]
hasattr(self, 'x') / assignment / 3 / self / stdout [True, 'assignment is a statement', 'defined', 'defined', True]
no yield/super probes a callback is a parameter; super() is explicit
11. sys._getframe().f_code.co_name inside whoami 'whoami'
12. Point.area.__qualname__ / inspect.ismethod(point.area) Geometry.Point.area / True
hasattr(Point, 'area') / ('_secret') / in vars(Point) True / True / True
'x' in vars(point) / 'z' in vars(point) True / False
13. type(point) / type(Point) / type(Geometry) Point / type / type
Point.__qualname__ / Point.__module__ 'Geometry.Point' / '__main__'
The inspect-style listing of an object's repr values leans on the repr/str split, which the Python library measures on its own page: repr is not str ↗.
Try it¶
- Write
describe(obj)that prints the object's class, its ancestors up toObject, every instance variable with its value, and every method the class itself defines with each parameter's name and kind; run it on a smallAccountclass that has an optional argument, a keyword argument, a splat and a**options. - Write
where_defined(obj, name)that returns"#{File.basename(file)}:#{line}"for a method, usingmethod(name).source_location— then try it onobj.method(:puts)and on[].method(:map). What doessource_locationreturn for a method written in C, and how would you tell such a method apart from one defined withdefine_methodfrom a block?
Solution to 1
introspection_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# introspection_kata_rb.rb — describe(obj): class, own methods with parameters, instance variables.
class Account
include Comparable
attr_reader :owner, :balance
def initialize(owner, balance = 0) = (@owner, @balance = owner, balance)
def deposit(amount, note: nil) = (@balance += amount; self)
def withdraw(amount, *fees, **options) = (@balance -= amount + fees.sum; self)
def <=>(other) = balance <=> other.balance
end
def describe(obj)
klass = obj.class
puts "class: #{klass}"
puts "ancestors: #{klass.ancestors.take_while { |a| a != Object }.inspect}"
puts "ivars: " + obj.instance_variables.map { |v| "#{v} = #{obj.instance_variable_get(v).inspect}" }.join(", ")
puts "methods:"
klass.instance_methods(false).sort.each do |name|
params = klass.instance_method(name).parameters.map { |kind, pname| "#{pname}:#{kind}" }
puts format(" %-10s (%s)", name, params.join(", "))
end
end
describe(Account.new("Ada", 100).deposit(50, note: "gift"))
Verified output of introspection_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
sendreaches private methods;public_senddoes not —respond_to?andmethod_defined?before dispatching a nameClass.newmakes a class; a constant names it —name,ancestorsandconstantsof a class you builtmethod_missingneedsrespond_to_missing?— whymethodsnever lists a ghost- Method objects and unbound methods —
MethodandUnboundMethodin full:owner,receiver,bind Bindingandeval—local_variablesthrough aBinding, and Python'slocals()- Instance variables are private — what
instance_variable_getsteps around - No docstrings — what
source_locationis used for when there is no__doc__ repris notstr↗ — the Python library on the two ways an object prints itself- 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… - Refinements are lexical, not global —
refine String do … endinside a module builds aRefinement, andusingthat module activates it from… - A backtrace is data you can read —
callerreturns an Array of Strings shapedfile:line:in 'Klass#method',caller_locationsreturns the…