OOP - Slots

Bemerkung

Der eigentliche Zweck für die Entwicklung __slots__ war ein Performancegewinn zu erzielen. „Slotted Classes“ sind etwas schnell was die Zugriffsgeschwindigkeit auf Attribute betrifft. Weiters brauchen diese weniger Speicher.

Oft ist zu lese das __slots__ Klassen sicherer machen, da Attribute nicht mehr dynamisch erstellt werden können. Das ist zwar nicht falsch war aber nicht der ursprüngliche Grund für die Entwicklung von __slots__.

Normale Klassen

Normale Python Klassen erlauben das dynamische Erstellen von Attributen.

class Person():
    def __init__(self,first_name,last_name):
        self.first_name = first_name
        self.last_name = last_name
p1 = Person("Max","Muster")
p1.__dict__
{'first_name': 'Max', 'last_name': 'Muster'}

Durch __dict__ erhalten wir die Instanzattribute in def Form eines Dictionary. Python erlaubt uns nun das dynamisch hinzufügen von Instanzattributen.

p1.age = 21
p1.__dict__
{'first_name': 'Max', 'last_name': 'Muster', 'age': 21}

Wie wir sehen ist nun das Attribute age hinzugefügt worden.

„Slotted“ Klassen

Die Verwendung von __slots__ verhindert die dynamische Erweiterung von Instanzattributen. Das hat wie oben besprochen folgende Vorteile:

  • Diese Instanzen benötigen weniger Speicher

  • Der Zugriff auf Instanzattributen ist schneller.

  • Es können nicht unbeabsichtigt Attribute hinzugefügt werden.

class SlottedPerson():
    __slots__ = ('first_name', 'last_name')
    def __init__(self,first_name,last_name):
        self.first_name = first_name
        self.last_name = last_name
slotted_p1 = SlottedPerson("Max","Muster")
slotted_p1.__slots__
('first_name', 'last_name')
# ps.age = 21 # --> attribute error
# ps.__dict__ # --> attribute error

Performenz Vergleich

import sys
class Person():
    def __init__(self,first_name,last_name):
        self.first_name = first_name
        self.last_name = last_name
p1 = Person("Max","Muster")
p1.__dict__
{'first_name': 'Max', 'last_name': 'Muster'}
class SlottedPerson():
    __slots__ = ('first_name', 'last_name')
    def __init__(self,first_name,last_name):
        self.first_name = first_name
        self.last_name = last_name
slotted_p1 = SlottedPerson("Max","Muster")
slotted_p1.__slots__
('first_name', 'last_name')
class PartSlottedPerson():
    __slots__ = ('first_name','__dict__')
    def __init__(self,first_name,last_name):
        self.first_name = first_name
        self.last_name = last_name
partslotted_p1 = PartSlottedPerson("Max","Muster")
print(partslotted_p1.__slots__)
print(partslotted_p1.__dict__)
('first_name', '__dict__')
{'last_name': 'Muster'}

Speicher

Bemerkung

pympler ist ein sehr gutes Python Modul zum Messen, Überwachen und Analysieren des Speicherverhaltens von Pyhton-Objekten. Dieses Modul eignet sich besser Benutzer Klassen zu analysieren als sys, ist jedoch nicht Teil der Standardbibliothek.

from pympler import asizeof
asizeof.asizeof(p1)
392
asizeof.asizeof(slotted_p1)
160
asizeof.asizeof(partslotted_p1)
328

Wir sehen ist die Instanz der „SlottedPerson“ Klasse deutlich kleiner als die „Person“ Klasse. pympler erlaubt uns noch mehr Details abzufragen.

print(asizeof.asized(p1, detail=1).format())
<__main__.Person object at 0x7f1e4c046d90> size=392 flat=48
    __dict__ size=344 flat=104
    __class__ size=0 flat=0
print(asizeof.asized(slotted_p1, detail=1).format())
<__main__.SlottedPerson object at 0x7f1e377d2940> size=160 flat=48
    first_name size=56 flat=56
    last_name size=56 flat=56
    __class__ size=0 flat=0
print(asizeof.asized(partslotted_p1, detail=1).format())
<__main__.PartSlottedPerson object at 0x7f1e377d2a90> size=328 flat=48
    __dict__ size=224 flat=104
    first_name size=56 flat=56
    __class__ size=0 flat=0

Hier sehen wir nun das der Overhead der der Klassen in unserem Fall gleich groß ist. Der Speicherunterschied erklärt sich also durch das Verwenden des __dict__.

print(392-344) # total_size - dict_size
print(160-2*56) # total_size - attribute_size
print(328-224-56) # total_size - dict_size - attribute_size
48
48
48

Geschwindigkeiten

Zum Messen der Geschwindigkeit können wir das Modul timeit aus der Standardbibliothek verwenden.

import timeit

Es werden die 3 Operation Schreiben, Lesen und das Löschen von Attributen durchgeführt.

def set_get_delete_func(obj):
    def set_get_del():
        obj.first_name = "Michael" # set
        obj.last_name = "Maier" # set
        obj.first_name # get
        obj.last_name # get
        del obj.first_name # del
        del obj.last_name # del
    return set_get_del
print(min(timeit.repeat(set_get_delete_func(p1))))
print(min(timeit.repeat(set_get_delete_func(slotted_p1))))
print(min(timeit.repeat(set_get_delete_func(partslotted_p1))))
print(max(timeit.repeat(set_get_delete_func(p1))))
print(max(timeit.repeat(set_get_delete_func(slotted_p1))))
print(max(timeit.repeat(set_get_delete_func(partslotted_p1))))
0.16847971300012432
0.12257865800347645
0.14542127300228458
0.1792634849989554
0.12501245699968422
0.16435634400113486

Wir können also sehen das Slotted Classes einen deutlichen Performancegewinn ermöglichen.

Fazit

Mit __slots__ lässt sich die Performance von Klassen verbessern, was das Ziel der Entwicklung war. Es sei aber angemerkt, dass es mittlerweile einige Optionen in Python gibt um die Performance von Klassen zu verbessern.

Vererbung von __slots__

Tipp

Die Vererbungsmechanismen von __slots__ sind durchaus komplex und werden im Kapitel Vererbung besprochen.