Skip to content

new is allocate then initialize

Level: 201 · anyone who has written def initialize and wondered who calls it

One line: Point.new(1, 2) is a class method that does two things — Class#allocate makes an empty object, then the automatically private instance method initialize(1, 2) fills it in — so allocate alone gives you an object with no instance variables, initialize's return value is ignored, and overriding self.new lets a class cache or dispatch its instances before any of that happens.

There is no constructor syntax in Ruby. new is an ordinary method on Class, which every class object inherits, and initialize is an ordinary instance method that new calls for you — it has to, because Ruby makes initialize private without being asked (along with initialize_copy). Everything the program below prints follows from that: what you can call on a freshly allocated object, why pt.initialize raises, why an ArgumentError from new names initialize's arity, and why super inside a self.new override is the only thing that ever runs initialize.

Verified output of new_allocate_and_initialize_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. new is allocate, then initialize
   (initialize(1, 2) runs; self is a Point)
   Point.new(1, 2)                              #<Point 1,2>
   Point.method(:new).owner                     Class
   Point.method(:allocate).owner                Class
2. initialize is private automatically
   Point.private_instance_methods(false).sort   [:initialize, :initialize_copy]
   (initialize(1, 2) runs; self is a Point)
   pt.initialize(3, 4)                          NoMethodError: private method 'initialize' called for an instance of Point
   (initialize(3, 4) runs; self is a Point)
   pt.send(:initialize, 3, 4); pt               #<Point 3,4>
3. allocate skips initialize
   Point.allocate                               #<Point nil,nil>
   blank.instance_variables                     []
   blank.x                                      nil
   (initialize(5, 6) runs; self is a Point)
   blank.send(:initialize, 5, 6); blank         #<Point 5,6>
4. initialize's return value is ignored
   (initialize(0, 0) runs; self is a Point)
   Point.new(0, 0).class                        Point
5. arguments go straight to initialize
   Point.new(1)                                 ArgumentError: wrong number of arguments (given 1, expected 2)
6. overriding self.new: a cache
   (Color#initialize("red") runs)
   (Color#initialize("blue") runs)
   Color.new("red").equal?(Color.new("red"))    true
   red.equal?(blue)                             false
7. overriding self.new: a factory
   Shape.new(:circle, 3)                        #<Circle size=3>
   Shape.new(:square, 4)                        #<Square size=4>
   Circle.new(5)                                #<Circle size=5>
8. initialize_copy runs on dup and clone
   (initialize(7, 8) runs; self is a Point)
   (initialize_copy runs, copying #<Point 7,8>)
   original.dup                                 #<Point 7,8>
   (initialize_copy runs, copying #<Point 7,8>)
   original.clone                               #<Point 7,8>
9. classes with no allocator or no new
   Integer.allocate                             TypeError: allocator undefined for Integer
   Integer.new                                  NoMethodError: undefined method 'new' for class Integer
   NilClass.new                                 NoMethodError: undefined method 'new' for class NilClass
10. a subclass calls super inside initialize
   (initialize(1, 2) runs; self is a Point3)
   Point3.new(1, 2, 3)                          #<Point3 1,2,3>
   Point3.instance_method(:initialize).arity    3

Reading the output

  • 1. Point.new(1, 2) prints the trace from inside initialize and returns the finished object. Both new and allocate are owned by Class, not by Point.
  • 2. private_instance_methods(false) lists initialize and initialize_copy, neither of which was declared private: Ruby makes them so. Calling pt.initialize(3, 4) from outside is therefore a NoMethodError, while send reaches it and re-initialises the object in place — see send and public_send.
  • 3. Point.allocate returns a Point with no instance variables, so its reader x is nil. A second send(:initialize, 5, 6) turns it into a normal point. This is what Marshal.load and object-relational mappers do: allocate, then fill in the state from elsewhere. (Not machine-checked here.)
  • 4. initialize returns :the_return_value_is_ignored; new still returns the object, so Point.new(0, 0).class is Point.
  • 5. new passes its arguments straight through, so the ArgumentError names initialize's expectation: given 1, expected 2.
  • 6. Color.new is overridden to look in a cache first. super is what reaches Class#new, so initialize runs once per colour: the trace shows "red" initialised once even though Color.new("red") was called twice, and the two calls return the same object (equal? is true).
  • 7. Shape.new(:circle, 3) dispatches to a subclass's new; the subclasses fall through to super, so Circle.new(5) still works directly.
  • 8. dup and clone do not call initialize. They call initialize_copy on the new object with the original as the argument — the hook to deep-copy a field. dup, clone and frozen state measures the rest.
  • 9. Some classes opt out. Integer.allocate raises TypeError: allocator undefined for Integer, and Integer.new and NilClass.new are NoMethodErrors: those classes undefine new, because integers are immediates and nil is the one instance of its class.
  • 10. A subclass's initialize calls super(x, y) explicitly; arity is 3, the subclass's own signature.

Compared with Python

Python splits the same two steps into two dunder methods and runs them from the metaclass: Point(1, 2) is type.__call__(Point, 1, 2), which calls Point.__new__(Point, 1, 2) — inherited from object here — and then __init__(1, 2) on the result. Row 2 is the first difference: __init__ is public, so pt.__init__(3, 4) simply re-initialises the object, and Python has no private methods for new to need send for. Row 3 is allocate by another name: object.__new__(Point) skips __init__, leaves vars(blank) empty, and a later blank.__init__(5, 6) fills it. Row 4 is a difference in the other direction — __init__ must return None, and returning 5 is a TypeError, where Ruby ignores initialize's value. Row 6 is the trap: a cache written in __new__ returns the cached object, but type.__call__ then runs __init__ on it again — the trace shows Color.__init__('red') twice — because __init__ runs whenever __new__ returns an instance of the class; Ruby's self.new override never calls initialize unless super does. Row 7's factory works in both, and for the same reason: __new__ returning a Circle (an instance of Shape) still gets __init__. Row 8: copy.copy calls __copy__ when the class defines it, and never __init__ — Ruby's initialize_copy is the same hook by another name, and copy.deepcopy falls back to a state copy when there is no __deepcopy__. Row 9: object(1) and int.__new__(str) are TypeErrors, and type(None)() hands back the one None rather than building a second one.

Verified output of new_allocate_and_initialize_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. calling the class is __new__, then __init__
   (__init__(1, 2) runs; self is a Point)
   Point(1, 2)                                  Point(1, 2)
   type(Point).__call__ is type.__call__        True
   Point.__new__ is object.__new__              True
2. __init__ is public
   '__init__' in vars(Point)                    True
   (__init__(1, 2) runs; self is a Point)
   (__init__(3, 4) runs; self is a Point)
   pt.__init__(3, 4); pt                        Point(3, 4)
   private methods                              none; a leading _ is a convention
3. object.__new__(Point) skips __init__
   object.__new__(Point)                        Point(None, None)
   vars(blank)                                  {}
   getattr(blank, 'x', None)                    None
   (__init__(5, 6) runs; self is a Point)
   blank.__init__(5, 6); blank                  Point(5, 6)
4. __init__ must return None
   BadInit()  (its __init__ returns 5)          TypeError
5. arguments go straight to __init__
   Point(1)                                     TypeError
6. overriding __new__: a cache (and a trap)
   (Color.__init__('red') runs)
   (Color.__init__('red') runs)
   (Color.__init__('blue') runs)
   Color('red') is Color('red')                 True
   red is blue                                  False
7. overriding __new__: a factory
   Shape('circle', 3)                           <Circle size=3>
   Shape('square', 4)                           <Square size=4>
   Circle(5)                                    <Circle size=5>
8. __copy__ runs on copy.copy; __init__ does not
   (__init__(7, 8) runs; self is a Point)
   (__copy__ runs, copying Point(7, 8))
   copy.copy(original)                          Point(7, 8)
   copy.deepcopy(original)                      Point(7, 8)
9. classes that refuse arguments or new instances
   object(1)                                    TypeError
   int.__new__(str)                             TypeError
   type(None)() is None                         True
10. a subclass calls super().__init__
   (__init__(1, 2) runs; self is a Point3)
   Point3(1, 2, 3)                              Point3(1, 2, 3)
   Point3.__init__.__code__.co_argcount - 1     3

Try it

  1. Write class Money with initialize(cents) and override self.new so that Money.new(0) always returns one shared zero object (built the first time it is asked for) while any other amount builds a fresh object. Print Money.new(0).equal?(Money.new(0)), Money.new(5).equal?(Money.new(5)) and how many times initialize ran; then call Money.allocate and show the count did not move.
  2. Give Point a class method from_hash(h) that builds a point with allocate and instance_variable_set, never calling initialize. Show that it prints the same as Point.new(h[:x], h[:y]), then add a puts inside initialize to prove it was skipped.
Solution to 1

new_allocate_and_initialize_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: a shared zero. Money.new(0) always returns one object, built once;
# every other amount is a fresh object. Count how often initialize runs, then
# show that allocate walks around the override entirely.

class Money
  @built = 0

  class << self
    attr_reader :built

    def new(cents)
      return @zero ||= super if cents.zero?
      super
    end

    def count_one = @built += 1
  end

  attr_reader :cents

  def initialize(cents)
    Money.count_one
    @cents = cents
  end

  def inspect = "#<Money #{@cents}c>"
end

zero_a = Money.new(0)
zero_b = Money.new(0)
five_a = Money.new(5)
five_b = Money.new(5)

puts "Money.new(0)                        #{zero_a.inspect}"
puts "Money.new(0).equal?(Money.new(0))   #{zero_a.equal?(zero_b)}"
puts "Money.new(5).equal?(Money.new(5))   #{five_a.equal?(five_b)}"
puts "times initialize ran                #{Money.built}"
puts "Money.allocate.cents                #{Money.allocate.cents.inspect}"
puts "times initialize ran after allocate #{Money.built}"

Verified output of new_allocate_and_initialize_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

Money.new(0)                        #<Money 0c>
Money.new(0).equal?(Money.new(0))   true
Money.new(5).equal?(Money.new(5))   false
times initialize ran                3
Money.allocate.cents                nil
times initialize ran after allocate 3

See also