How does pickle create an instance in defiance of the constructor?

Viewed 52

My ultimate goal is to have a class that has a regular constructor, but also a second constructor that can be used to create partially instantiated versions of the class.

class Foo(object):
   def __init__(self, foo, bar):
      self.foo = foo
      self.bar = bar
   def create_partial(self, **kwargs):
      self.foo = kwargs.get('foo', None)
      self.bar = kwargs.get('bar', None)

I noticed that pickle achieves this somehow. You can write __getstate__ and __setstate__ functions that only deal with one of the parameters and it ignores the fact you haven't defined one of them. For example

class Foo(object):
  def__init__(self, foo, bar):
    self.foo = foo
    self.bar = bar
  def __getstate__(self):
    return {'foo': self.foo}
  def __setstate__(self, d):
    self.foo = d['foo']

Then I can pickle and unpickle an instance to get a semi instantiated object

myfoo = pickle.loads(pickle.dumps(Foo(1,2)))
print(myfoo.foo) # 1
print(myfoo.bar) # throws AttributeError
type(myfoo) # __main__.Foo

So, my question is, how does it manage to get a blank instance of the class, half instantiate it and have it still report its type as Foo, further, how can I do the same without having to go through pickle?

P.S. I know this is an insane thing to want to do, but I want to know how

2 Answers

When you run Foo(), we think of this as calling Foo.__init__(). But really, there's an additional step before that, which creates the object. foo = Foo(a, b, c) is roughly equivalent to

foo = Foo.__new__(Foo, a, b, c)
foo.__init__(a, b, c)

__new__ is a rarely-used magic method that actually constructs the object. You almost never need to override it (the most common use case for overriding it is when subclassing builtin types like float), and most classes inherit the default object.__new__. We can call that latter function directly.

# Assuming that Foo doesn't override __new__
myfoo = Foo.__new__(Foo)
# Or, to be extra safe
myfoo = object.__new__(Foo)

Note that __new__ takes the intended type as the first parameter, so even though we call object.__new__, we still get a Foo since we passed Foo as the parameter. No constructors were harmed in the process.

You can call Foo.__new__(Foo) to create your object without going through __init__.

Related