When working with Ruby, every once in a while you'll find yourself messing with a bunch of strings which are the names of classes. Given these strings, you'll need to go and instantiate the appropriate classes - something like taking "Array", "File" or "Booga" and figuring out how to call
The usual suspect -
The first time I needed to do this, I did a Google search without actually thinking about it too much. The number one result was
But
Using
Why do I say it's a hack? Well, if for some obscure reason you decided to create a class which was not a named constant, then you can't get hold of it with
But what about
Today, Srihari asked me if he could load up classes using
Here
Performance of
Let's take a look at the performance of
The
Now, back to
Here's the implementation of
Summary
Array.new
, File.new
or... well, you get the picture. If you're wondering when you'd ever need this, just try instantiating controllers at runtime based on the urls being requested.The usual suspect -
Module#const_get
The first time I needed to do this, I did a Google search without actually thinking about it too much. The number one result was
Module.const_get
which does pretty much what is needed.irb(main):005:0> Module.const_get('Array').new
=> []
As the docs would tell you, this returns the named constant which matches the string, which is pretty much what classes are - named constants which are instances of the Class
class. Well, it worked, and beyond reading that this wouldn't work for classes which are nested in modules (const_get
doesn't know or care about parsing stuff like the ::
in Net::HTTP
), I didn't bother too much. I did, however, come across something called Kernel#qualified_const_get
which gets around this limitation, but more on that later. But
Module#const_get
is a hackUsing
const_get
is effectively a hack - it uses the fact that class names are also constants to allow you to get hold of them. Why do I say it's a hack? Well, if for some obscure reason you decided to create a class which was not a named constant, then you can't get hold of it with
const_get
. Here's an example:ooga = Class.new # Create a class the hard way
ooga.class_eval do # Add a method to it the hard way
def hello
"Hello!"
end
end
p ooga.new.hello # Prove that ooga can be instantiated
p Module.const_get('Array').new # As can 'Array', a named constant, using const_get
p Module.const_get('ooga').new.hello # But not 'ooga', which isn't
Output:"Hello!"
[]
temp05.rb:10:in `const_get': wrong constant name ooga (NameError)
from temp05.rb:10
But what about
eval
?Today, Srihari asked me if he could load up classes using
eval
. I said, 'Just use Module.const_get', but of course we were curious so we tried using eval
and it worked. Obviously, given that eval
effectively allows you to interpret code at runtime, it also handles nested classes and/or modules. Here's a code sample showing a regular invocation, an invocation using eval and an invocation using const_get
(which fails) of a nested class:puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
module Ooga
class Booga
def hello
"hello!"
end
end
end
puts Ooga::Booga.new
puts eval('Ooga::Booga').new
puts Module.const_get('Ooga::Booga').new
Output:temp04.rb:15:in `const_get': wrong constant name Ooga::Booga (NameError)
from temp04.rb:15
Ruby 1.8.6, 2007-06-07, i486-linux
#<Ooga::Booga:0xb7c84774>
#<Ooga::Booga:0xb7c8465c>
Here
Booga
is a class nested in the module Ooga
and as you can see const_get
fails to fetch it because it isn't a constant (heck, it isn't even the right syntax for a constant).Performance of
const_get
and eval
Let's take a look at the performance of
const_get
versus eval
, an important factor if you're doing this inside a loop or some such.puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
require 'benchmark'
n = 1000000
Benchmark.bmbm(10) do |rpt|
rpt.report("simple invocation") do
n.times {Array.new}
end
rpt.report("const_get invocation") do
n.times {Kernel.const_get('Array').new}
end
rpt.report("eval invocation") do
n.times {eval('Array').new}
end
end
Output:
Ruby 1.8.6, 2007-06-07, i486-linux Rehearsal -------------------------------------------------------- simple invocation 0.910000 0.090000 1.000000 ( 1.013326) const_get invocation 1.400000 0.110000 1.510000 ( 1.513216) eval invocation 3.480000 0.220000 3.700000 ( 3.692692) ----------------------------------------------- total: 6.210000sec user system total real simple invocation 0.890000 0.100000 0.990000 ( 1.000915) const_get invocation 1.440000 0.080000 1.520000 ( 1.514948) eval invocation 3.490000 0.200000 3.690000 ( 3.689998)
const_get
takes 1.5 times longer, while eval
takes more than 3 times as long as a simple invocation.The
Kernel#qualified_const_get
alternativeNow, back to
Kernel#qualified_const_get
which was created by Gregory in this blog post a couple of years ago. It's looks a lot like const_get
but is capable of figuring out nested classes too and it works just fine. However, it's very slow (it isn't native C code) and should probably be named something else because it has nothing to do with fetching constants any more. Kernel#fetch_class
perhaps? But some numbers first:puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
require 'benchmark'
require 'qualified_const_get'
module Ooga
class Booga
def hello
"hello!"
end
end
end
n = 1000000
Benchmark.bmbm(10) do |rpt|
rpt.report("simple invocation") do
n.times {Ooga::Booga.new}
end
rpt.report("qualified const_get invocation") do
n.times {Kernel.qualified_const_get('Ooga::Booga').new}
end
rpt.report("eval invocation") do
n.times {eval('Ooga::Booga').new}
end
end
Output:
Ruby 1.8.6, 2007-06-07, i486-linux Rehearsal ------------------------------------------------------- simple invocation 0.860000 0.120000 0.980000 ( 0.991182) qualified_const_get 16.620000 2.330000 18.950000 ( 19.052966) eval invocation 4.500000 0.230000 4.730000 ( 4.741436) --------------------------------------------- total: 24.660000sec user system total real simple invocation 0.990000 0.100000 1.090000 ( 1.090303) qualified_const_get 17.290000 2.420000 19.710000 ( 19.735733) eval invocation 4.250000 0.170000 4.420000 ( 4.429670)See what I mean about
qualified_const_get
being slow because it isn't native?Here's the implementation of
Kernel#qualified_const_get
quoted from his blog:# http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html
module Kernel
def qualified_const_get(str)
path = str.to_s.split('::')
from_root = path[0].empty?
if from_root
from_root = []
path = path[1..-1]
else
start_ns = ((Class === self)||(Module === self)) ? self : self.class
from_root = start_ns.to_s.split('::')
end
until from_root.empty?
begin
return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
rescue NameError
from_root.delete_at(-1)
end
end
path.inject(Object) { |ns,name| ns.const_get(name) }
end
end
Summary
- There are three choices when trying to convert a string to the corresponding class:
Kernel#const_get
,eval
andKernel#qualified_const_get
Kernel#const_get
is the fastest, doesn't handle nested classes and works for all but the weirdest of scenarios (the class you're trying to get hold of isn't a named constant)eval
is significantly slower, but it works in any situationKernel#qualified_const_get
is abysmally slow, but handles nested classes. However, until there is a native implementation, it loses toeval
on every front
8 comments:
I've been experimenting with JRuby and found that using const_get is just unreliable, while eval works fine. For example, a java class com.foo.bar.Baz is represented as Java::FooBar::Baz. If you do const_get on Java::FooBar, you get a string "Java::FooBarConst_get". It appears that these Java package modules must define method_missing to just concatenate the method name to the module name. I haven't investigated further.
Anyway, using eval works fine.
Not exactly faster, but another approach is to iterate through the ObjectSpace collection, and instantiate the matching class:
require 'my_package/my_class'
class Factory
def self.get_object(o)
ObjectSpace.each_object(Class) do |c|
return c.new() if c.name.eql?(o)
end
return nil
end
end
#usage
require 'factory'
o = Factory.get_object("MyPackage::MyClass")
If you have a string containing a potential class name and you're not sure if its formatted correctly for whatever reason before you use 'eval' or 'Kernel.const_get', you can use 'Kernel.subclasses_of(ActiveRecord::Base).member?(YourClass)' to check if it exists so you don't get a 'LoadError: ./activesupport-2.3.2/lib/active_support/dependencies.rb:426:in `load_missing_constant''
Have you tried "MyClassName".constantize.new ???
Yes, I have!!! But #constantize is a Rails (activesupport) extension, so without Rails, no #constantize.
I added the Rails 2.3.5 constantize to the benchmarks above, and in my results it's over 7 times slower than the "simple invocation" and 4 times slower than const_get.
I haven't benchmarked this, but you can also do:
Module.const_get("ModuleName").const_get("ClassName")
Sidu and Travis: thanks for clarifying this. I am able to use Travis' way to retrieve a class nested within a module.
Post a Comment