Wykorzystanie __slots__?

Odpowiedzi:

1018

W Pythonie jaki jest cel __slots__i w jakich przypadkach należy tego unikać?

TLDR:

Specjalny atrybut __slots__pozwala jawnie określić, które atrybuty instancji mają mieć instancje obiektu, z oczekiwanymi rezultatami:

  1. szybszy dostęp do atrybutów.
  2. oszczędność miejsca w pamięci.

Oszczędność miejsca pochodzi z

  1. Przechowywanie referencji wartości w gniazdach zamiast __dict__.
  2. Odmawianie __dict__i __weakref__tworzenie, jeśli klasy nadrzędne odmawiają ich, a ty deklarujesz __slots__.

Szybkie ostrzeżenia

Małe zastrzeżenie, powinieneś zadeklarować konkretny slot tylko raz w drzewie spadkowym. Na przykład:

class Base:
    __slots__ = 'foo', 'bar'

class Right(Base):
    __slots__ = 'baz', 

class Wrong(Base):
    __slots__ = 'foo', 'bar', 'baz'        # redundant foo and bar

Python nie sprzeciwia się, gdy popełnisz błąd (prawdopodobnie powinien), problemy mogą się w inny sposób nie ujawnić, ale twoje obiekty zajmą więcej miejsca niż powinny. Python 3.8:

>>> from sys import getsizeof
>>> getsizeof(Right()), getsizeof(Wrong())
(56, 72)

Wynika to z faktu, że deskryptor gniazda Bazy ma gniazdo inne niż Błędne. To zwykle nie powinno się pojawiać, ale może:

>>> w = Wrong()
>>> w.foo = 'foo'
>>> Base.foo.__get__(w)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: foo
>>> Wrong.foo.__get__(w)
'foo'

Największym zastrzeżeniem jest wielokrotne dziedziczenie - nie można łączyć wielu „klas nadrzędnych z niepustymi gniazdami”.

Aby dostosować się do tego ograniczenia, postępuj zgodnie z najlepszymi praktykami: Uwzględnij abstrakcję wszystkich oprócz jednego lub wszystkich rodziców, które ich konkretna klasa i twoja nowa konkretna klasa zbiorowo odziedziczą - dając abstrakcje puste miejsca (podobnie jak abstrakcyjne klasy podstawowe w biblioteka standardowa).

Przykład zawiera sekcja dotycząca wielokrotnego dziedziczenia poniżej.

Wymagania:

  • Aby atrybuty o nazwie in __slots__były przechowywane w szczelinach zamiast a __dict__, klasa musi dziedziczyć po object.

  • Aby zapobiec utworzeniu a __dict__, musisz dziedziczyć, objecta wszystkie klasy w spadku muszą zadeklarować __slots__i żadna z nich nie może mieć '__dict__'pozycji.

Jest wiele szczegółów, jeśli chcesz dalej czytać.

Dlaczego warto korzystać __slots__: Szybszy dostęp do atrybutów.

Twórca Pythona, Guido van Rossum, twierdzi , że stworzył go w __slots__celu szybszego dostępu do atrybutów.

Trywialne jest zademonstrowanie wymiernie szybszego dostępu:

import timeit

class Foo(object): __slots__ = 'foo',

class Bar(object): pass

slotted = Foo()
not_slotted = Bar()

def get_set_delete_fn(obj):
    def get_set_delete():
        obj.foo = 'foo'
        obj.foo
        del obj.foo
    return get_set_delete

i

>>> min(timeit.repeat(get_set_delete_fn(slotted)))
0.2846834529991611
>>> min(timeit.repeat(get_set_delete_fn(not_slotted)))
0.3664822799983085

Dostęp szczelinowy jest prawie 30% szybszy w Pythonie 3.5 na Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

W Pythonie 2 w systemie Windows zmierzyłem go o około 15% szybciej.

Dlaczego warto korzystać __slots__: Oszczędności pamięci

Innym celem __slots__jest zmniejszenie miejsca w pamięci zajmowanego przez każdą instancję obiektu.

Mój własny wkład w dokumentację jasno wskazuje powody tego :

Przestrzeń zaoszczędzona przy użyciu __dict__może być znacząca.

SQLAlchemy przypisuje wiele oszczędności pamięci __slots__.

Aby to zweryfikować, użyj dystrybucji Anaconda w Pythonie 2.7 w systemie Ubuntu Linux, używając guppy.hpy(aka heapy) isys.getsizeof , a rozmiar instancji klasy bez __slots__zadeklarowanego i nic innego to 64 bajty. To nie obejmuje __dict__. Dziękuję Pythonowi za leniwą ocenę, __dict__najwyraźniej nie jest wywoływana, dopóki nie zostanie przywołana, ale klasy bez danych są zwykle bezużyteczne. Po wywołaniu ten __dict__atrybut ma dodatkowo minimum 280 bajtów.

Natomiast instancja klasy z __slots__zadeklarowanym jako ()(bez danych) ma tylko 16 bajtów i 56 bajtów ogółem z jednym elementem w szczelinach, 64 z dwoma.

Dla 64-bitowego Pythona, ilustruję zużycie pamięci w bajtach w Pythonie 2.7 i 3.6 __slots__ i __dict__(nie zdefiniowano gniazd) dla każdego punktu, w którym dykt rośnie w 3.6 (z wyjątkiem atrybutów 0, 1 i 2):

       Python 2.7             Python 3.6
attrs  __slots__  __dict__*   __slots__  __dict__* | *(no slots defined)
none   16         56 + 272   16         56 + 112 | if __dict__ referenced
one    48         56 + 272    48         56 + 112
two    56         56 + 272    56         56 + 112
six    88         56 + 1040   88         56 + 152
11     128        56 + 1040   128        56 + 240
22     216        56 + 3344   216        56 + 408     
43     384        56 + 3344   384        56 + 752

Tak więc, pomimo mniejszych nagrań w Pythonie 3, widzimy, jak ładnie __slots__skaluje się instancje, aby zaoszczędzić nam pamięć, i to jest główny powód, dla którego chciałbyś skorzystać __slots__.

Dla kompletności moich notatek zauważ, że istnieje jednorazowy koszt na boks w klasie nazw 64 bajty w Pythonie 2 i 72 bajty w Pythonie 3, ponieważ automaty wykorzystują deskryptory danych takie jak właściwości, zwane „członkami”.

>>> Foo.foo
<member 'foo' of 'Foo' objects>
>>> type(Foo.foo)
<class 'member_descriptor'>
>>> getsizeof(Foo.foo)
72

Demonstracja __slots__:

Aby odmówić utworzenia a __dict__, musisz podklasę object:

class Base(object): 
    __slots__ = ()

teraz:

>>> b = Base()
>>> b.a = 'a'
Traceback (most recent call last):
  File "<pyshell#38>", line 1, in <module>
    b.a = 'a'
AttributeError: 'Base' object has no attribute 'a'

Lub podklasę innej klasy, która definiuje __slots__

class Child(Base):
    __slots__ = ('a',)

i teraz:

c = Child()
c.a = 'a'

ale:

>>> c.b = 'b'
Traceback (most recent call last):
  File "<pyshell#42>", line 1, in <module>
    c.b = 'b'
AttributeError: 'Child' object has no attribute 'b'

Aby zezwolić na __dict__tworzenie przy podzklasowaniu obiektów szczelinowych, po prostu dodaj '__dict__'do __slots__(pamiętaj, że szczeliny są uporządkowane i nie powinieneś powtarzać tych, które są już w klasach nadrzędnych):

class SlottedWithDict(Child): 
    __slots__ = ('__dict__', 'b')

swd = SlottedWithDict()
swd.a = 'a'
swd.b = 'b'
swd.c = 'c'

i

>>> swd.__dict__
{'c': 'c'}

Lub nie musisz nawet deklarować __slots__w swojej podklasie i nadal będziesz korzystać z miejsc od rodziców, ale nie będziesz ograniczać tworzenia __dict__:

class NoSlots(Child): pass
ns = NoSlots()
ns.a = 'a'
ns.b = 'b'

I:

>>> ns.__dict__
{'b': 'b'}

Jednakże, __slots__może powodować problemy dla wielu dziedziczenia:

class BaseA(object): 
    __slots__ = ('a',)

class BaseB(object): 
    __slots__ = ('b',)

Ponieważ utworzenie klasy podrzędnej od rodziców z obydwoma niepustymi miejscami:

>>> 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

Jeśli napotkasz ten problem, mógłby po prostu usunąć __slots__z rodziców, lub jeśli masz kontrolę rodziców, dać im pustych szczelin lub Refactor do abstrakcji:

from abc import ABC

class AbstractA(ABC):
    __slots__ = ()

class BaseA(AbstractA): 
    __slots__ = ('a',)

class AbstractB(ABC):
    __slots__ = ()

class BaseB(AbstractB): 
    __slots__ = ('b',)

class Child(AbstractA, AbstractB): 
    __slots__ = ('a', 'b')

c = Child() # no problem!

Dodaj, '__dict__'aby __slots__uzyskać dynamiczne przypisanie:

class Foo(object):
    __slots__ = 'bar', 'baz', '__dict__'

i teraz:

>>> foo = Foo()
>>> foo.boink = 'boink'

Tak więc '__dict__'w przypadku automatów do gier tracimy niektóre korzyści związane z rozmiarem, ponieważ zaletą jest dynamiczne przypisywanie i wciąż mają automaty do nazw, których oczekujemy.

Kiedy dziedziczysz po obiekcie, który nie jest dzielony, otrzymujesz ten sam rodzaj semantyki podczas używania __slots__- nazwy, które __slots__wskazują wartości wycinane, podczas gdy wszelkie inne wartości są umieszczane w instancji __dict__.

Unikanie, __slots__ponieważ chcesz mieć możliwość dodawania atrybutów w locie, nie jest właściwie dobrym powodem - po prostu dodaj "__dict__"do swojego__slots__ jeśli jest to wymagane.

Możesz również dodać __weakref__do __slots__jawnie, jeśli potrzebujesz tej funkcji.

Ustaw na pustą krotkę podczas tworzenia podklucza o nazwietuple:

Wbudowane namedtuple tworzą niezmienne instancje, które są bardzo lekkie (zasadniczo rozmiar krotek), ale aby uzyskać korzyści, musisz zrobić to sam, jeśli je podklasujesz:

from collections import namedtuple
class MyNT(namedtuple('MyNT', 'bar baz')):
    """MyNT is an immutable and lightweight object"""
    __slots__ = ()

stosowanie:

>>> nt = MyNT('bar', 'baz')
>>> nt.bar
'bar'
>>> nt.baz
'baz'

A próba przypisania nieoczekiwanego atrybutu podnosi, AttributeErrorponieważ zapobiegliśmy tworzeniu __dict__:

>>> nt.quux = 'quux'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyNT' object has no attribute 'quux'

Państwo może umożliwić __dict__stworzenie pozostawiając off __slots__ = (), ale nie można używać niepusty __slots__z podtypów krotki.

Największe zastrzeżenie: wielokrotne dziedziczenie

Nawet jeśli puste pola są takie same dla wielu rodziców, nie można ich używać razem:

class Foo(object): 
    __slots__ = 'foo', 'bar'
class Bar(object):
    __slots__ = 'foo', 'bar' # alas, would work if empty, i.e. ()

>>> class Baz(Foo, Bar): pass
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    multiple bases have instance lay-out conflict

__slots__Wydaje się, że użycie pustego elementu nadrzędnego zapewnia największą elastyczność, umożliwiając dziecku wybranie opcji zapobiegania lub zezwalania (poprzez dodanie w '__dict__'celu uzyskania dynamicznego przypisania, patrz sekcja powyżej) utworzenie__dict__ :

class Foo(object): __slots__ = ()
class Bar(object): __slots__ = ()
class Baz(Foo, Bar): __slots__ = ('foo', 'bar')
b = Baz()
b.foo, b.bar = 'foo', 'bar'

Nie musisz mieć slotów - więc jeśli je dodasz i usuniesz później, nie powinno to powodować żadnych problemów.

Idąc w opałach tutaj : Jeśli komponowania wstawek lub za pomocą abstrakcyjnych klas bazowych , które nie są przeznaczone do wystąpienia, pusty __slots__w tych rodziców, wydaje się być najlepszym sposobem, aby przejść pod względem elastyczności subclassers.

Aby to zademonstrować, najpierw stwórzmy klasę z kodem, którego chcielibyśmy używać w ramach wielokrotnego dziedziczenia

class AbstractBase:
    __slots__ = ()
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'

Możemy użyć powyższego bezpośrednio, dziedzicząc i deklarując oczekiwane miejsca:

class Foo(AbstractBase):
    __slots__ = 'a', 'b'

Ale nas to nie obchodzi, to trywialne pojedyncze dziedziczenie, potrzebujemy innej klasy, z której moglibyśmy dziedziczyć, może z hałaśliwym atrybutem:

class AbstractBaseC:
    __slots__ = ()
    @property
    def c(self):
        print('getting c!')
        return self._c
    @c.setter
    def c(self, arg):
        print('setting c!')
        self._c = arg

Teraz, jeśli obie bazy miały niepuste gniazda, nie moglibyśmy wykonać poniższych czynności. (W rzeczywistości, gdybyśmy chcieli, moglibyśmy podać AbstractBaseniepuste przedziały aib i pozostawić je poza poniższą deklaracją - pozostawienie ich byłoby błędne):

class Concretion(AbstractBase, AbstractBaseC):
    __slots__ = 'a b _c'.split()

A teraz mamy funkcjonalność zarówno poprzez wielokrotne dziedziczenie, jak i nadal możemy odmawiać __dict__i __weakref__tworzyć instancje:

>>> c = Concretion('a', 'b')
>>> c.c = c
setting c!
>>> c.c
getting c!
Concretion('a', 'b')
>>> c.d = 'd'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Concretion' object has no attribute 'd'

Inne przypadki, aby uniknąć automatów:

  • Unikaj ich, gdy chcesz wykonać __class__ zadanie z inną klasą, która ich nie ma (i nie możesz ich dodać), chyba że układy miejsc są identyczne. (Jestem bardzo zainteresowany tym, kto to robi i dlaczego).
  • Unikaj ich, jeśli chcesz podklasować wbudowane zmienne długości, takie jak long, krotka lub str, i chcesz do nich dodać atrybuty.
  • Unikaj ich, jeśli nalegasz na podanie wartości domyślnych za pomocą atrybutów klasy dla zmiennych instancji.

Być może będziesz w stanie wysunąć dalsze zastrzeżenia z pozostałej części __slots__ dokumentacji (dokumenty deweloperów w wersji 3.7 są najbardziej aktualne) , do których w znacznym stopniu przyczyniłem się ostatnio.

Krytyka innych odpowiedzi

Aktualne najlepsze odpowiedzi przytaczają nieaktualne informacje i są dość falujące i pomijają ten znak w ważnych sprawach.

Nie używaj „tylko __slots__podczas tworzenia wielu obiektów”

Cytuję:

„Chcesz użyć, __slots__jeśli chcesz utworzyć wiele (setki, tysiące) obiektów tej samej klasy”.

Abstrakcyjne klasy podstawowe, na przykład z collectionsmodułu, nie są tworzone, ale __slots__są dla nich zadeklarowane.

Dlaczego?

Jeśli użytkownik chce zaprzeczyć __dict__lub __weakref__utworzyć, te rzeczy nie mogą być dostępne w klasach nadrzędnych.

__slots__ przyczynia się do ponownego użycia podczas tworzenia interfejsów lub miksów.

Prawdą jest, że wielu użytkowników Pythona nie pisze w celu ponownego użycia, ale kiedy to robisz, opcja odmowy niepotrzebnego wykorzystania miejsca jest cenna.

__slots__ nie przerywa wytrawiania

Podczas trawienia wycinanego obiektu może się wydawać, że narzeka wprowadzającym w błąd TypeError:

>>> pickle.loads(pickle.dumps(f))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled

To jest właściwie niepoprawne. Ta wiadomość pochodzi od najstarszego protokołu, który jest domyślny. Możesz wybrać najnowszy protokół z -1argumentem. W Pythonie 2.7 byłoby to 2(które zostało wprowadzone w 2.3), a w 3.6 tak 4.

>>> pickle.loads(pickle.dumps(f, -1))
<__main__.Foo object at 0x1129C770>

w Python 2.7:

>>> pickle.loads(pickle.dumps(f, 2))
<__main__.Foo object at 0x1129C770>

w Pythonie 3.6

>>> pickle.loads(pickle.dumps(f, 4))
<__main__.Foo object at 0x1129C770>

Chciałbym o tym pamiętać, ponieważ jest to rozwiązany problem.

Krytyka (do 2 października 2016 r.) Zaakceptowała odpowiedź

Pierwszy akapit to w połowie krótkie wyjaśnienie, w połowie przewidywalne. Oto jedyna część, która faktycznie odpowiada na pytanie

Właściwe użycie __slots__to oszczędność miejsca w obiektach. Zamiast dynamicznego dyktowania, które pozwala dodawać atrybuty do obiektów w dowolnym momencie, istnieje statyczna struktura, która nie pozwala na dodawanie po utworzeniu. Oszczędza to narzut jednego dyktowania dla każdego obiektu, który korzysta ze szczelin

Druga połowa to pobożne życzenia, a poza tym:

Chociaż jest to czasem przydatna optymalizacja, byłoby całkowicie niepotrzebne, gdyby interpreter Pythona był wystarczająco dynamiczny, aby wymagał dyktowania tylko wtedy, gdy rzeczywiście istniały dodatki do obiektu.

Python faktycznie robi coś podobnego, tworząc tylko __dict__wtedy, gdy jest dostępny, ale tworzenie wielu obiektów bez danych jest dość śmieszne.

Drugi akapit nadmiernie upraszcza i omija rzeczywiste powody, których należy unikać __slots__. Poniżej nie jest prawdziwy powód do unikania automatów (z rzeczywistych powodów, zobacz resztę mojej odpowiedzi powyżej):

Zmieniają zachowanie obiektów, które mają gniazda, w sposób, który może być nadużywany przez maniaków sterowania i statycznych drobiazgów pisania.

Następnie omawia inne sposoby osiągnięcia tego perwersyjnego celu z Pythonem, nie omawiając nic wspólnego z tym __slots__ .

Trzeci akapit to bardziej pobożne życzenia. Razem jest to w większości nietypowa treść, której autor nawet nie napisał i przyczynia się do amunicji dla krytyków strony.

Dowody użycia pamięci

Utwórz niektóre normalne obiekty i obiekty szczelinowe:

>>> class Foo(object): pass
>>> class Bar(object): __slots__ = ()

Utwórz ich milion:

>>> foos = [Foo() for f in xrange(1000000)]
>>> bars = [Bar() for b in xrange(1000000)]

Sprawdź za pomocą guppy.hpy().heap():

>>> guppy.hpy().heap()
Partition of a set of 2028259 objects. Total size = 99763360 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0 1000000  49 64000000  64  64000000  64 __main__.Foo
     1     169   0 16281480  16  80281480  80 list
     2 1000000  49 16000000  16  96281480  97 __main__.Bar
     3   12284   1   987472   1  97268952  97 str
...

Uzyskaj dostęp do zwykłych obiektów i ich __dict__i sprawdź ponownie:

>>> for f in foos:
...     f.__dict__
>>> guppy.hpy().heap()
Partition of a set of 3028258 objects. Total size = 379763480 bytes.
 Index  Count   %      Size    % Cumulative  % Kind (class / dict of class)
     0 1000000  33 280000000  74 280000000  74 dict of __main__.Foo
     1 1000000  33  64000000  17 344000000  91 __main__.Foo
     2     169   0  16281480   4 360281480  95 list
     3 1000000  33  16000000   4 376281480  99 __main__.Bar
     4   12284   0    987472   0 377268952  99 str
...

Jest to zgodne z historią Pythona, od typów i klas Unifying w Pythonie 2.2

Jeśli podklasujesz typ wbudowany, do instancji automatycznie dodawane jest dodatkowe miejsce, aby pomieścić __dict__i __weakrefs__. (To __dict__nie jest inicjowane, dopóki go nie użyjesz, więc nie powinieneś martwić się o miejsce zajmowane przez pusty słownik dla każdej tworzonej instancji.) Jeśli nie potrzebujesz tej dodatkowej przestrzeni, możesz dodać wyrażenie „ __slots__ = []” do Twoja klasa.

Aaron Hall
źródło
14
łał, jedna cholera odpowiedź - dzięki! Jednak nie złapałem class Child(BaseA, BaseB): __slots__ = ('a', 'b')przykładu z rodzicami empy-slot. Dlaczego tutaj dictproxystworzono zamiast zbierać AttributeErrorfor c?
Skandix,
@Skandix dzięki za zwrócenie mojej uwagi na literówkę, okazało się, że to nie była instancja, prawdopodobnie zapomniałem, że edytowałem tę część, kiedy zapisałem ją w historii postów. Prawdopodobnie zostałby złapany wcześniej, gdybym zrobił to dobrze i sprawił, że kod był łatwiejszy do skopiowania ... Dzięki jeszcze raz!
Aaron Hall
38
Ta odpowiedź powinna być częścią oficjalnej dokumentacji Pythona __slots__. Poważnie! Dziękuję Ci!
NightElfik
13
@NightElfik wierzcie lub nie, włączyłem się do dokumentacji Pythona __slots__około rok temu: github.com/python/cpython/pull/1819/files
Aaron Hall
Fantastycznie szczegółowa odpowiedź. Mam jedno pytanie: czy należy używać gniazd domyślnie, chyba że korzystanie z nich dotknie jednego z ostrzeżeń, czy też jest to coś, co należy rozważyć, jeśli wiesz, że będziesz walczył o szybkość / pamięć? Innymi słowy, czy zachęcić początkującego do zapoznania się z nimi i korzystania z nich od samego początku?
freethebees
265

Cytując Jacoba Hallena :

Właściwe użycie __slots__to oszczędność miejsca w obiektach. Zamiast dynamicznego dyktowania, które pozwala dodawać atrybuty do obiektów w dowolnym momencie, istnieje statyczna struktura, która nie pozwala na dodawanie po utworzeniu. [Takie zastosowanie __slots__eliminuje narzut jednego dykta dla każdego obiektu.] Chociaż jest to czasem przydatna optymalizacja, byłoby zupełnie niepotrzebne, gdyby interpreter Pythona był wystarczająco dynamiczny, aby wymagał dyktowania tylko wtedy, gdy rzeczywiście istniały dodatki do obiekt.

Niestety na automatach występuje efekt uboczny. Zmieniają zachowanie obiektów, które mają gniazda, w sposób, który może być nadużywany przez maniaków sterowania i statycznych drobiazgów pisania. To źle, ponieważ maniacy kontrolni powinni nadużywać metaklas, a statyczne pisanie czcionek powinno nadużywać dekoratorów, ponieważ w Pythonie powinien istnieć tylko jeden oczywisty sposób na zrobienie czegoś.

Sprawienie, by CPython był wystarczająco inteligentny, aby poradzić sobie z oszczędnością miejsca, __slots__jest dużym przedsięwzięciem i prawdopodobnie dlatego nie ma go na liście zmian dla P3k (jeszcze).

Jeff Bauer
źródło
86
Chciałbym zobaczyć trochę opracowania na temat „pisania na klawiaturze statycznej” / dekoratora, bez pejoratyw. Cytowanie nieobecnych stron trzecich jest nieprzydatne. __slots__nie rozwiązuje tych samych problemów, co pisanie statyczne. Na przykład w C ++ nie deklaracja zmiennej członka jest ograniczona, lecz przypisanie do niej zmiennej niezamierzonego typu (i wymuszonego kompilatora). Nie akceptuję korzystania __slots__, tylko zainteresowany rozmową. Dzięki!
hiwaylon
126

Będziesz chciał użyć, __slots__jeśli zamierzasz utworzyć wiele (setki, tysiące) obiektów tej samej klasy. __slots__istnieje tylko jako narzędzie do optymalizacji pamięci.

Odradza się stosowanie __slots__do ograniczania tworzenia atrybutów.

Wytrawianie przedmiotów za pomocą __slots__ nie będzie działać z domyślnym (najstarszym) protokołem ; konieczne jest podanie późniejszej wersji.

Niektóre inne funkcje introspekcji Pythona również mogą zostać naruszone.

Ryan
źródło
10
W mojej odpowiedzi demonstruję wytrawianie obiektu w szczelinę, a także adresuję pierwszą część twojej odpowiedzi.
Aaron Hall
2
Rozumiem twój punkt widzenia, ale automaty oferują również szybszy dostęp do atrybutów (jak twierdzili inni). W takim przypadku nie trzeba „tworzyć wielu (setek, tysięcy) obiektów tej samej klasy” w celu uzyskania wydajności. Zamiast tego potrzebujesz wielu dostępów do tego samego (szczelinowego) atrybutu tej samej instancji. (Proszę, popraw mnie, jeśli się mylę.)
Rotareti,
61

Każdy obiekt python ma __dict__atrybut, który jest słownikiem zawierającym wszystkie inne atrybuty. np. kiedy wpiszesz self.attrpython faktycznie robi self.__dict__['attr']. Jak możesz sobie wyobrazić używanie słownika do przechowywania atrybutu zajmuje trochę więcej miejsca i czasu na dostęp do niego.

Jednak podczas używania __slots__dowolny obiekt utworzony dla tej klasy nie będzie miał __dict__atrybutu. Zamiast tego dostęp do wszystkich atrybutów odbywa się bezpośrednio za pomocą wskaźników.

Jeśli więc chcesz mieć strukturę w stylu C, a nie pełnoprawną klasę, możesz jej użyć __slots__do kompaktowania wielkości obiektów i skrócenia czasu dostępu do atrybutów. Dobrym przykładem jest klasa Point zawierająca atrybuty x & y. Jeśli masz dużo punktów, możesz spróbować użyć __slots__, aby zaoszczędzić trochę pamięci.

Suraj
źródło
10
Nie, instancja klasy ze __slots__zdefiniowaną strukturą nie przypomina struktury w stylu C. Istnieją nazwy atrybutów odwzorowujące słownik na poziomie klasy na indeksy, w przeciwnym razie nie byłoby możliwe: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Naprawdę uważam, że ta odpowiedź powinna zostać wyjaśniona (mogę to zrobić, jeśli chcesz). Nie jestem też pewien, czy instance.__hidden_attributes[instance.__class__[attrname]]to jest szybsze niż instance.__dict__[attrname].
tzot
22

Oprócz innych odpowiedzi, oto przykład użycia __slots__:

>>> class Test(object):   #Must be new-style class!
...  __slots__ = ['x', 'y']
... 
>>> pt = Test()
>>> dir(pt)
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__', 
 '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', 
 '__repr__', '__setattr__', '__slots__', '__str__', 'x', 'y']
>>> pt.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: x
>>> pt.x = 1
>>> pt.x
1
>>> pt.z = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute 'z'
>>> pt.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Test' object has no attribute '__dict__'
>>> pt.__slots__
['x', 'y']

Tak więc, aby zaimplementować __slots__, wymaga tylko dodatkowej linii (i uczynienia twojej klasy klasą nowego stylu, jeśli jeszcze nie jest). W ten sposób można pięciokrotnie zmniejszyć ślad pamięci tych klas , kosztem pisania niestandardowego kodu piklowania, jeśli i kiedy będzie to konieczne.

Jewgienij Siergiejew
źródło
11

Sloty są bardzo przydatne dla wywołań biblioteki, aby wyeliminować „nazwaną metodę wysyłania” podczas wykonywania wywołań funkcji. Jest to wspomniane w dokumentacji SWIG . W przypadku bibliotek o wysokiej wydajności, które chcą zmniejszyć obciążenie funkcji w przypadku często nazywanych funkcji za pomocą gniazd, jest to znacznie szybsze.

Teraz może to nie być bezpośrednio związane z pytaniem PO. Jest bardziej związany z budowaniem rozszerzeń niż z użyciem składni szczelin na obiekcie. Ale pomaga uzupełnić obraz wykorzystania gniazd i niektóre uzasadnienia za nimi.

Demolishun
źródło
7

Atrybut instancji klasy ma 3 właściwości: instancję, nazwę atrybutu i wartość atrybutu.

W zwykłym dostępie do atrybutów instancja działa jak słownik, a nazwa atrybutu działa jako klucz w wartości wyszukiwania tego słownika.

instancja (atrybut) -> wartość

W dostępie __slots__ nazwa atrybutu działa jak słownik, a instancja jako klucz w wyszukiwaniu wartości w słowniku.

atrybut (instancja) -> wartość

We wzorze flyweight nazwa atrybutu działa jak słownik, a wartość działa jako klucz w tym słowniku, patrząc na instancję.

atrybut (wartość) -> instancja

Dmitrij Rubanowicz
źródło
Jest to dobry udział i nie pasuje dobrze w komentarzu do jednej z odpowiedzi, które również sugerują wagi latające, ale nie jest to pełna odpowiedź na samo pytanie. W szczególności (tylko w kontekście pytania): dlaczego Flyweight i „w jakich przypadkach należy unikać ...” __slots__?
Merlyn Morgan-Graham
@Merlyn Morgan-Graham, służy jako wskazówka do wyboru: regularny dostęp, __slots__ lub flyweight.
Dmitrij Rubanowicz
3

Bardzo prosty przykład __slot__atrybutu.

Problem: bez __slots__

Jeśli nie mam __slot__atrybutu w swojej klasie, mogę dodawać nowe atrybuty do moich obiektów.

class Test:
    pass

obj1=Test()
obj2=Test()

print(obj1.__dict__)  #--> {}
obj1.x=12
print(obj1.__dict__)  # --> {'x': 12}
obj1.y=20
print(obj1.__dict__)  # --> {'x': 12, 'y': 20}

obj2.x=99
print(obj2.__dict__)  # --> {'x': 99}

Jeśli spojrzeć na powyższym przykładzie widać, że obj1 i obj2 mają własne x i y atrybuty i pyton stworzył również dictatrybut dla każdego obiektu ( obj1 i obj2 ).

Załóżmy, że jeśli mój test klasy ma tysiące takich obiektów? Utworzenie dodatkowego atrybutu dictdla każdego obiektu spowoduje duże obciążenie (pamięć, moc obliczeniowa itp.) W moim kodzie.

Rozwiązanie: z __slots__

Teraz w poniższym przykładzie test mojej klasy zawiera __slots__atrybut. Teraz nie mogę dodawać nowych atrybutów do moich obiektów (oprócz atrybutu x), a Python nie tworzy dictjuż atrybutu. Eliminuje to narzut dla każdego obiektu, który może stać się znaczący, jeśli masz wiele obiektów.

class Test:
    __slots__=("x")

obj1=Test()
obj2=Test()
obj1.x=12
print(obj1.x)  # --> 12
obj2.x=99
print(obj2.x)  # --> 99

obj1.y=28
print(obj1.y)  # --> AttributeError: 'Test' object has no attribute 'y'
N Randhawa
źródło
2

Innym, nieco niejasnym zastosowaniem __slots__jest dodawanie atrybutów do obiektowego proxy z pakietu ProxyTypes, wcześniej części projektu PEAK. Jego ObjectWrapperpozwala na pełnomocnika innego obiektu, ale osią wszystkich interakcji z proxy obiektu. Nie jest to bardzo często używane (i nie obsługuje Python 3), ale użyliśmy go do implementacji bezpiecznego blokowania wątków wokół implementacji asynchronicznej opartej na tornado, która odbija cały dostęp do obiektu proxy przez ioloop, używając bezpiecznego wątku concurrent.Futureobiekty do synchronizacji i zwracania wyników.

Domyślnie dowolny dostęp do atrybutu do obiektu proxy daje wynik z obiektu proxy. Jeśli chcesz dodać atrybut do obiektu proxy, __slots__możesz go użyć.

from peak.util.proxies import ObjectWrapper

class Original(object):
    def __init__(self):
        self.name = 'The Original'

class ProxyOriginal(ObjectWrapper):

    __slots__ = ['proxy_name']

    def __init__(self, subject, proxy_name):
        # proxy_info attributed added directly to the
        # Original instance, not the ProxyOriginal instance
        self.proxy_info = 'You are proxied by {}'.format(proxy_name)

        # proxy_name added to ProxyOriginal instance, since it is
        # defined in __slots__
        self.proxy_name = proxy_name

        super(ProxyOriginal, self).__init__(subject)

if __name__ == "__main__":
    original = Original()
    proxy = ProxyOriginal(original, 'Proxy Overlord')

    # Both statements print "The Original"
    print "original.name: ", original.name
    print "proxy.name: ", proxy.name

    # Both statements below print 
    # "You are proxied by Proxy Overlord", since the ProxyOriginal
    # __init__ sets it to the original object 
    print "original.proxy_info: ", original.proxy_info
    print "proxy.proxy_info: ", proxy.proxy_info

    # prints "Proxy Overlord"
    print "proxy.proxy_name: ", proxy.proxy_name
    # Raises AttributeError since proxy_name is only set on 
    # the proxy object
    print "original.proxy_name: ", proxy.proxy_name
NeilenMarais
źródło
1

Zasadniczo nie masz pożytku __slots__.

W chwili, gdy wydaje ci się, że możesz potrzebować __slots__, faktycznie chcesz użyć wzorów lekkich lub Flyweight . Są to przypadki, gdy nie chcesz już używać obiektów czysto Pythona. Zamiast tego potrzebujesz obiektowego opakowania w języku Python wokół tablicy, struktury lub tablicy numpy.

class Flyweight(object):

    def get(self, theData, index):
        return theData[index]

    def set(self, theData, index, value):
        theData[index]= value

Opakowanie podobne do klasy nie ma atrybutów - zapewnia jedynie metody, które działają na dane bazowe. Metody można sprowadzić do metod klasowych. Rzeczywiście, można to sprowadzić do samych funkcji działających na bazowej tablicy danych.

S.Lott
źródło
17
Z czym ma związek Flyweight __slots__?
oefe
3
@ oefe: Na pewno nie dostaję twojego pytania. Mogę zacytować moją odpowiedź, jeśli to pomoże „gdy myślisz, że możesz potrzebować automatów , naprawdę chcesz użyć… Wzorca projektowego Flyweight”. Właśnie to ma wspólnego Flyweight z automatami . Czy masz bardziej szczegółowe pytanie?
S.Lott
21
@ oefe: Flyweight i __slots__obie techniki optymalizacji w celu oszczędzania pamięci. __slots__pokazuje korzyści, gdy masz wiele obiektów, a także wzór wzornictwa Flyweight. Oba rozwiązują ten sam problem.
jfs
7
Czy jest dostępne porównanie między używaniem gniazd i korzystaniem z Flyweight pod względem zużycia pamięci i szybkości?
kontulai
8
Chociaż Flyweight jest z pewnością przydatny w niektórych kontekstach, wierzcie lub nie, odpowiedź na „jak mogę zmniejszyć zużycie pamięci w Pythonie podczas tworzenia obiektów zillionowych” nie zawsze „nie używaj Pythona dla swoich obiektów zillionowych”. Czasami __slots__naprawdę jest odpowiedzią i jak zauważa Evgeni, można ją dodać jako prostą refleksję (np. Możesz najpierw skupić się na poprawności, a następnie zwiększyć wydajność).
Patrick Maupin,
0

Pierwotne pytanie dotyczyło ogólnych przypadków użycia nie tylko pamięci. Należy tutaj wspomnieć, że uzyskuje się także lepszą wydajność podczas tworzenia dużych ilości obiektów - co jest interesujące np. Podczas analizowania dużych dokumentów w obiektach lub z bazy danych.

Oto porównanie tworzenia drzew obiektów z milionem wpisów, przy użyciu szczelin i bez szczelin. Jako odniesienie również wydajność przy użyciu zwykłych dykt dla drzew (Py2.7.10 w OSX):

********** RUN 1 **********
1.96036410332 <class 'css_tree_select.element.Element'>
3.02922606468 <class 'css_tree_select.element.ElementNoSlots'>
2.90828204155 dict
********** RUN 2 **********
1.77050495148 <class 'css_tree_select.element.Element'>
3.10655999184 <class 'css_tree_select.element.ElementNoSlots'>
2.84120798111 dict
********** RUN 3 **********
1.84069895744 <class 'css_tree_select.element.Element'>
3.21540498734 <class 'css_tree_select.element.ElementNoSlots'>
2.59615707397 dict
********** RUN 4 **********
1.75041103363 <class 'css_tree_select.element.Element'>
3.17366290092 <class 'css_tree_select.element.ElementNoSlots'>
2.70941114426 dict

Klasy testowe (ident, appart from slots):

class Element(object):
    __slots__ = ['_typ', 'id', 'parent', 'childs']
    def __init__(self, typ, id, parent=None):
        self._typ = typ
        self.id = id
        self.childs = []
        if parent:
            self.parent = parent
            parent.childs.append(self)

class ElementNoSlots(object): (same, w/o slots)

kod testowy, pełny tryb:

na, nb, nc = 100, 100, 100
for i in (1, 2, 3, 4):
    print '*' * 10, 'RUN', i, '*' * 10
    # tree with slot and no slot:
    for cls in Element, ElementNoSlots:
        t1 = time.time()
        root = cls('root', 'root')
        for i in xrange(na):
            ela = cls(typ='a', id=i, parent=root)
            for j in xrange(nb):
                elb = cls(typ='b', id=(i, j), parent=ela)
                for k in xrange(nc):
                    elc = cls(typ='c', id=(i, j, k), parent=elb)
        to =  time.time() - t1
        print to, cls
        del root

    # ref: tree with dicts only:
    t1 = time.time()
    droot = {'childs': []}
    for i in xrange(na):
        ela =  {'typ': 'a', id: i, 'childs': []}
        droot['childs'].append(ela)
        for j in xrange(nb):
            elb =  {'typ': 'b', id: (i, j), 'childs': []}
            ela['childs'].append(elb)
            for k in xrange(nc):
                elc =  {'typ': 'c', id: (i, j, k), 'childs': []}
                elb['childs'].append(elc)
    td = time.time() - t1
    print td, 'dict'
    del droot
Czerwona pigułka
źródło