Search code examples
pythoninitializationslots

How to use `__slots__` with initialization of attributes?


I read through the main answers on usage of slots and it has given me an idea of how and where to use __slots__ .

Now, I am porting a code from Python 2 to Python 3 which is similar to as following:

class B(object):
    __slots__ = ('_fields')
    _fields = set()

But this is giving error Python 3 while working fine on Python 2:

ValueError: '_fields' in __slots__ conflicts with class variable.

I change the code to

class B(object):
    __slots__ = ('_fields')
    def __init__(self):
        _fields = set()

and it works fine. My query is, is it even the correct change ?

As i got from original code, I guess it is saying that don't use __dict__ for saving memory or faster access or whatever reason but at the same time is also trying to specify the type of attribute _field as set(). Can the change above be the right way to say it or it can have some side effects.


Further Experiments: I experimented further with following variants (on Python 3):

import pdb

class A(object):
    a = set()

'''
class B(object):
    __slots__ = ('a')
    a = set()
'''

class C(object):
    __slots__ = ('a')
    def __init__(self):
        a = set()

class D(object):
    def __init__(self):
        __slots__ = ('a')
        a = set()

if __name__ == '__main__':
    #pdb.set_trace()
    x = A(); print(dir(x))
    #y = B()
    z = C(); print(dir(z))
    z1 = D(); print(dir(z1))

and it gave following output.

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'a']


['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'a']


['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

We can see that only C object shows correct footprint i.e. no __dict__ and only __slots__ . Isn't it what ideally we would want ? Any explanation on __weakref__ would also be helpful.

Also on Python 2, both B and C object show same footprint. Based on that should C be the right way to put it as it is compiling on both Python 2 and 3 as well.


Solution

  • But this is giving error Python 3 while working fine on Python 2:

    ValueError: '_fields' in __slots__ conflicts with class variable.

    While you didn't get an error in Python2 at class creation/compile time like in Py3k, if you try to actually set the value of _fields, you get AttributeError: 'C' object attribute '_fields' is read-only:

    >>> class C(object):
    ...   __slots__ = ('_fields')
    ...   _fields = set()
    ...
    >>>
    >>> c = C()
    >>> c._fields
    set([])
    >>> c._fields = 'foo'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'C' object attribute '_fields' is read-only
    >>>
    

    Also, see the fourth note in the slots documentation:

    __slots__ are implemented at the class level by creating descriptors (Implementing Descriptors) for each variable name. As a result, class attributes cannot be used to set default values for instance variables defined by __slots__; otherwise, the class attribute would overwrite the descriptor assignment.


    Wrt your modification:

    I change the code to

    class B(object):
        __slots__ = ('_fields')
        def __init__(self):
            _fields = set()
    

    The modified class B has a _fields inside __init__(), not self._fields so it's just a local variable in init, and not accessible anywhere else in the class. Change that to:

     class B(object):
        __slots__ = ('_fields')
        def __init__(self):
            self._fields = set()
    

    To correctly initialise _fields, do this:

     class B(object):
         __slots__ = ('_fields')
         def __init__(self, _fields=None):
             if not _fields:
                 self._fields = set()
    

    Wrt further experimentation:

    In class D, __slots__ is a variable only inside __init()__. It's not the (special) class variable D.__slots__; or even the instance variable self.__slots__. So it has __dict__.

    Class A has none, so also has __dict__.

    Class C has __slots__ correctly.