Python is a dynamic programming language. Here you're allowed to do whatever you wish with objects and their instances - you can add attributes, remove them etc.
E.g.
class Teapot:
pass
>>> obj = Teapot()
>>> obj.sugar_cubes_count = 15
>>> obj.sugar_cubes_count
# 15
>>> obj.leaves = ["Mint", "Chamomile"]
>>> obj.leaves
["Mint", "Chamomile"]
Of course, in most cases you don't want to do this. Anyway, this ability is really convenient and gives a lot of flexibility... But.. We're going to pay for this. Pay with a deacresed attributes speed access and increased memory consumption.
But what if we don't need so flexible approach? What if we're ok to declare every attribute manually - can we avoid the price then? Yes, we can.
This is where magic attribute __slot__ comes in. He allows to limit a set of available attributes for an object
class SugarCube:
__slots__ = ('width', 'height')
>>> obj = SugarCube()
>>> obj.width = 5
>>> obj.width
# 5
>>> obj.color = 'Brown'
Traceback (most recent call last):
File "python", line 5, in <module>
AttributeError: 'SugarCube' object has no attribute 'color'
So, since now we're not allowed to add custom attributes to instances. Will it increase the performance?
class Foo: __slots__ = ('foo',)
class Bar: pass
def get_set_delete(obj):
obj.foo = 'foo'
obj.foo
del obj.foo
def test_foo():
get_set_delete(Foo())
def test_bar():
get_set_delete(Bar())
and let's estimate the execution time using timeit module
>>> import timeit
>>> min(timeit.repeat(test_foo))
0.18728888700024982
>>> min(timeit.repeat(test_bar))
0.23361162799983504
So, the class with __slots__ is 20% faster on the attribute accessioning. The numbers, of course, can be different for different versions of interpretator, OS etc.
What about memory? Every object instance has magic attribute __dict__. This is a dictionary of all available attributes of the instance. Every instance has it.
class BigLebowski:
pass
>>> obj = BigLebowski()
>>> obj.__dict__
# {}
>>> obj.milk = True
>>> obj.__dict__
# {'milk': True}
When the __slots__ attribute is declared explicitly, the __dict__dictionary won't exist:
class LasVegas:
__slots__ = ('fear', 'loathing')
>>> obj = SlotsClass()
>>> obj.fear = 5
>>> obj.__slots__
# ('fear', 'loathing')
>>> obj.__dict__
Traceback (most recent call last):
File "python", line 8, in <module>
AttributeError: 'LasVegas' object has no attribute '__dict__'
This is how the memory is saved
Attributes count | Using __slots__ (bytes) | No using __slots__ (bytes) |
---|---|---|
0 | 32 | 152 |
1 | 64 | 232 |
2 | 104 | 320 |
6 | 264 | 712 |
11 | 464 | 1240 |
22 | 904 | 2376 |
43 | 1744 | 4568 |
As you can see, __slots__ are easy... BUT.... there are some things you should consider:
- Inheritance
__slots__ value inherits, but doesn't prevent of __dict__ creation. So, child classes will allow to add dynamic attributes.class Fruit: __slots__ = ('color', 'shape') class Cucumber(Fruit): pass >>> obj = Cucumber() >>> obj.__slots__ # ('color', 'shape') >>> obj.color = 'Green' >>> obj.is_vegatable = False >>> obj.__dict__ # {'is_vegatable': False}
class Fruit: __slots__ = ('color', 'shape') class Cucumber(Fruit): __slots__ = ('is_vegetable', ) >>> obj = Cucumber() >>> obj.color = 'Green' >>> obj.is_vegetable = False >>> obj.something_new = 3 Traceback (most recent call last): File "python", line 12, in <module> AttributeError: 'Cucumber' object has no attribute 'something_new'
- It's not going to work with multiple inheritance. If there are two base classes with declared __slots__ attribute, an attempt to create a child class will fail.
class BaseA(object): __slots__ = ('a',) class BaseB(object): __slots__ = ('b',) >>> class Child(BaseA, BaseB): __slots__ = () Traceback (most recent call last): File "<pyshell#68>", line 1, in <module> class Child(BaseA, BaseB): __slots__ = () TypeError: Error when calling the metaclass bases multiple bases have instance lay-out conflict
- The last problem - classes with defined __slots__ attribute doesn't support weakrefs. It's caused by the fact, that declaring __slots__ prevents of creation __dict__ method as well as of __weakref__ method. The last one provides weakref support. The solution is simple - you just need to add __weakref__ into the __slots__ list