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_getThe 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:10But 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 evalLet'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,evalandKernel#qualified_const_get Kernel#const_getis 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)evalis significantly slower, but it works in any situationKernel#qualified_const_getis abysmally slow, but handles nested classes. However, until there is a native implementation, it loses toevalon 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