Copy with __slots__

Alex Martelli aleax at aleax.it
Wed Sep 18 09:16:28 EDT 2002


Griebel, Peer wrote [I'm only leaving the relevant parts of the script]:

> class C1(object):
>     __slots__ = "s1";
> 
> class C2(C1):
>     __slots__ = "s2";

Funny use of semicolons here.  I'd suggest omitting them!

> o2 = C2()
> 
> import copy
> p2 = copy.copy(o2)
> print p2.s2           # attribute s2 does not exist (has not been copied)
> 
> The last print statement fails with the an AttributeError. Since

Traceback (most recent call last):
  File "sloco.py", line 25, in ?
    p2 = copy.copy(o2)
  File "/usr/local/lib/python2.2/copy.py", line 84, in copy
    y = _reconstruct(x, reductor(), 0)
  File "/usr/local/lib/python2.2/copy_reg.py", line 68, in _reduce
    dict = getstate()
TypeError: a class that defines __slots__ without defining __getstate__ 
cannot be pickled

This is with Python 2.2.1 -- as you see, it doesn't get anywhere
as far as the print statement.  The error message is somewhat
peculiar (who's trying to _pickle_ anything...?) -- and if you
do try to define __getstate__ (and possibly __setstate__), you'll
see they're no use for copy.copy (nor copy.deepcopy).  _reconstruct
gets in the way.

If you only care about copy.copy, the simplest alternative is:

class C2(C1):
    __slots__ = "s2"
    def __copy__(self):
        x = self.__class__()
        x.s1 = self.s1
        x.s2 = self.s2
        return x

You can of course also add try/except AttributeError around each of the 
assignments "x.sN = self.sN", if you want to let an instance of C2 be 
copied even when one or both of its attributes are not yet defined.
For deepcopy, you could use a similar special method __deepcopy__ (needs
to deepcopy the attributes, of course).

The way to cover copy.copy, copy.deepcopy AND pickle is to have the
class supply a special method __reduce__.  __reduce__ can return a
pair where the first item is callable, the second item is the tuple
to pass as arguments to the first.  Thus, for example:

class C2(C1):
    __slots__ = "s2"
    def _build(s1, s2):
        x = C2()
        x.s1 = s1
        x.s2 = s2
        return x
    _build = staticmethod(_build)
    def __reduce__(self):
        return C2._build, (self.s1, self.s2)

Here, _build need not worry about deep-copying -- if needed, it's
been performed previously, on the args _build gets passed.


The nicest thing about it is that a class with slots will often
already supply some nice function to build instances of the class
given values to put in the slots -- you don't necessarily have
to write factory functions such as _build just for the purpose
of supporting copy, deepcopy and pickle... you often have them
lying around!-).  For example, a more typical case might be:

class C2(C1):
    __slots__ = 's2'
    def __init__(self, s1=None, s2=None):
        C1.__init__(self, s1)
        self.s2 = s2
    def __reduce__(self):
        return self.__class__, (self.s1, self.s2)

Here I'm assuming C1 has a similar __init__, but that's a detail.
The point is that __reduce__ can use class C2 itself as the
first item of its result -- the class IS callable, after all.

I'm specifically using self.__class__ rather than a literal C2
here as a tiny enhancement -- if a class C3 subclasses C2 and
does not override __init__ in a way that requires arguments
different from the slots' values, then C3 need not override
__reduce__ either -- it can just inherit it from C2.  Not the
end of the world, but sometimes handy.


Alex




More information about the Python-list mailing list