Penggunaan __slots__?

Jawaban:

1019

Dengan Python, apa tujuan __slots__dan kasus apa yang harus dihindari seseorang?

TLDR:

Atribut khusus __slots__memungkinkan Anda untuk secara eksplisit menyatakan atribut instance yang Anda harapkan dari instance objek Anda, dengan hasil yang diharapkan:

  1. akses atribut lebih cepat .
  2. penghematan ruang dalam memori.

Penghematan ruang berasal

  1. Menyimpan referensi nilai dalam slot alih-alih __dict__.
  2. Menyangkal __dict__dan membuat __weakref__jika kelas induk menolaknya dan Anda menyatakan __slots__.

Peringatan cepat

Peringatan kecil, Anda hanya boleh mendeklarasikan slot tertentu satu kali di pohon warisan. Sebagai contoh:

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

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

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

Python tidak keberatan ketika Anda melakukan kesalahan ini (mungkin seharusnya), masalah mungkin tidak dinyatakan, tetapi objek Anda akan memakan lebih banyak ruang daripada yang seharusnya. Python 3.8:

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

Ini karena deskriptor slot Base memiliki slot terpisah dari Wrong's. Ini biasanya tidak muncul, tetapi bisa:

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

Peringatan terbesar adalah untuk beberapa warisan - beberapa "kelas induk dengan slot kosong" tidak dapat digabungkan.

Untuk mengakomodasi pembatasan ini, ikuti praktik terbaik: Faktorkan semua kecuali satu atau semua abstraksi orang tua yang masing-masing kelas konkretnya dan kelas beton baru Anda akan diwarisi dari - memberikan slot kosong abstraksi (seperti kelas dasar abstrak di kelas). perpustakaan standar).

Lihat bagian tentang beberapa warisan di bawah untuk contoh.

Persyaratan:

  • Agar atribut yang dinamai __slots__benar-benar disimpan dalam slot, bukan a __dict__, kelas harus mewarisi darinya object.

  • Untuk mencegah pembuatan a __dict__, Anda harus mewarisi dari objectdan semua kelas dalam pewarisan harus menyatakan __slots__dan tidak ada dari mereka yang dapat memiliki '__dict__'entri.

Ada banyak detail jika Anda ingin terus membaca.

Mengapa menggunakan __slots__: Akses atribut lebih cepat.

Pencipta Python, Guido van Rossum, menyatakan bahwa ia sebenarnya diciptakan __slots__untuk akses atribut yang lebih cepat.

Itu sepele untuk menunjukkan akses cepat signifikan yang terukur:

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

dan

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

Akses yang ditempatkan hampir 30% lebih cepat di Python 3.5 di Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

Dalam Python 2 pada Windows saya mengukurnya sekitar 15% lebih cepat.

Mengapa menggunakan __slots__: Penghematan Memori

Tujuan lain __slots__adalah untuk mengurangi ruang dalam memori yang digunakan oleh setiap instance objek.

Kontribusi saya sendiri untuk dokumentasi dengan jelas menyatakan alasan di balik ini :

Ruang yang dihemat saat menggunakan __dict__bisa signifikan.

SQLAlchemy menghubungkan banyak penghematan memori __slots__.

Untuk memverifikasi ini, gunakan distribusi Anaconda dari Python 2.7 di Ubuntu Linux, dengan guppy.hpy(alias heapy) dan sys.getsizeof, ukuran instance kelas tanpa __slots__dideklarasikan, dan tidak ada yang lain, adalah 64 byte. Itu tidak termasuk __dict__. Terima kasih Python untuk evaluasi malas lagi, yang __dict__tampaknya tidak dipanggil sebelum direferensikan, tetapi kelas tanpa data biasanya tidak berguna. Ketika dipanggil, __dict__atribut adalah minimum 280 byte.

Sebaliknya, instance kelas dengan __slots__dinyatakan sebagai ()(tidak ada data) hanya 16 byte, dan 56 total byte dengan satu item dalam slot, 64 dengan dua.

Untuk 64 bit Python, saya menggambarkan konsumsi memori dalam byte di Python 2.7 dan 3.6, untuk __slots__dan __dict__(tidak ada slot yang ditentukan) untuk setiap titik di mana dikt tumbuh dalam 3,6 (kecuali untuk atribut 0, 1, dan 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

Jadi, meskipun ada dikte kecil di Python 3, kita melihat seberapa baik __slots__skala untuk contoh menghemat memori kita, dan itulah alasan utama yang ingin Anda gunakan __slots__.

Hanya untuk melengkapi catatan saya, perhatikan bahwa ada biaya satu kali per slot di namespace kelas dari 64 byte di Python 2, dan 72 byte di Python 3, karena slot menggunakan deskriptor data seperti properti, yang disebut "anggota".

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

Demonstrasi dari __slots__:

Untuk menolak pembuatan a __dict__, Anda harus subkelas object:

class Base(object): 
    __slots__ = ()

sekarang:

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

Atau subkelas kelas lain yang mendefinisikan __slots__

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

dan sekarang:

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

tapi:

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

Untuk mengizinkan __dict__kreasi sambil mensubklasifikasikan objek yang ditempatkan, cukup tambahkan '__dict__'ke __slots__(perhatikan bahwa slot dipesan, dan Anda tidak boleh mengulangi slot yang sudah ada di kelas induk):

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

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

dan

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

Atau Anda bahkan tidak perlu mendeklarasikan __slots__di subclass Anda, dan Anda masih akan menggunakan slot dari orang tua, tetapi tidak membatasi penciptaan __dict__:

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

Dan:

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

Namun, __slots__dapat menyebabkan masalah bagi banyak warisan:

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

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

Karena membuat kelas anak dari orang tua dengan kedua slot tidak kosong gagal:

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

Jika Anda mengalami masalah ini, Anda bisa menghapus __slots__dari orang tua, atau jika Anda memiliki kendali atas orang tua, beri mereka slot kosong, atau refactor untuk abstraksi:

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!

Tambahkan '__dict__'ke __slots__untuk mendapatkan penugasan dinamis:

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

dan sekarang:

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

Jadi dengan '__dict__'slot kami kehilangan beberapa manfaat ukuran dengan sisi positif memiliki tugas dinamis dan masih memiliki slot untuk nama yang kami harapkan.

Saat Anda mewarisi dari objek yang tidak ditempatkan, Anda mendapatkan jenis semantik yang sama saat Anda menggunakan __slots__- nama yang __slots__mengarah ke nilai yang ditempatkan, sementara nilai lainnya diletakkan di instance __dict__.

Menghindari __slots__karena Anda ingin dapat menambahkan atribut dengan cepat sebenarnya bukan alasan yang baik - tambahkan saja "__dict__"ke Anda __slots__jika ini diperlukan.

Anda sama dapat menambahkan __weakref__untuk __slots__secara eksplisit jika Anda perlu fitur itu.

Setel untuk mengosongkan tuple ketika mensubkripkan sebuah namesupuple:

Namanya builtup membuat contoh abadi yang sangat ringan (pada dasarnya, ukuran tupel) tetapi untuk mendapatkan manfaatnya, Anda perlu melakukannya sendiri jika Anda mensubklasifikasikan:

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

pemakaian:

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

Dan mencoba menetapkan atribut yang tidak terduga memunculkan sebuah AttributeErrorkarena kami telah mencegah pembuatan __dict__:

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

Anda dapat mengizinkan __dict__pembuatan dengan meninggalkan __slots__ = (), tetapi Anda tidak dapat menggunakan non-kosong __slots__dengan subtipe tuple.

Peringatan Terbesar: Warisan berganda

Bahkan ketika slot tidak kosong sama untuk beberapa orang tua, slot tersebut tidak dapat digunakan bersama:

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

Menggunakan kosong __slots__di induk tampaknya memberikan fleksibilitas yang paling, memungkinkan anak untuk memilih untuk mencegah atau mengizinkan (dengan menambahkan '__dict__'untuk mendapatkan tugas yang dinamis, lihat bagian di atas) penciptaan__dict__ :

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

Anda tidak harus memiliki slot - jadi jika Anda menambahkannya, dan menghapusnya nanti, itu tidak akan menyebabkan masalah.

Keluar mengambil risiko di sini : Jika Anda membuat mixin atau menggunakan kelas dasar abstrak , yang tidak dimaksudkan untuk dipakai, kosong __slots__pada orang tua itu tampaknya menjadi cara terbaik untuk pergi dalam hal fleksibilitas untuk subclasser.

Untuk mendemonstrasikan, pertama, mari kita buat kelas dengan kode yang ingin kita gunakan di bawah banyak pewarisan

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)})'

Kita dapat menggunakan hal di atas secara langsung dengan mewarisi dan mendeklarasikan slot yang diharapkan:

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

Tapi kami tidak peduli tentang itu, itu warisan tunggal yang sepele, kami membutuhkan kelas lain yang mungkin juga kami warisi, mungkin dengan atribut berisik:

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

Sekarang jika kedua pangkalan memiliki slot kosong, kami tidak bisa melakukan di bawah ini. (Faktanya, jika kita mau, kita bisa memberikan AbstractBaseslot kosong a dan b, dan meninggalkannya dari deklarasi di bawah ini - membiarkannya salah):

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

Dan sekarang kami memiliki fungsionalitas dari keduanya melalui multiple inheritance, dan masih dapat menolak __dict__dan membuat __weakref__instance:

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

Kasus lain untuk menghindari slot:

  • Hindari mereka ketika Anda ingin melakukan __class__tugas dengan kelas lain yang tidak memilikinya (dan Anda tidak dapat menambahkannya) kecuali tata letak slot identik. (Saya sangat tertarik mempelajari siapa yang melakukan ini dan mengapa.)
  • Hindari mereka jika Anda ingin subkelas builtin panjang variabel seperti panjang, tuple, atau str, dan Anda ingin menambahkan atribut ke dalamnya.
  • Hindari mereka jika Anda bersikeras memberikan nilai default melalui atribut kelas untuk variabel instan.

Anda mungkin dapat menarik peringatan lebih lanjut dari sisa __slots__ dokumentasi (dokumentasi 3,7 dev adalah yang terbaru) , yang telah saya berikan kontribusi signifikan baru-baru ini.

Kritik dari jawaban lain

Jawaban teratas saat ini mengutip informasi yang sudah ketinggalan zaman dan sangat bergelombang dan kehilangan tanda dalam beberapa hal penting.

Jangan "hanya gunakan __slots__saat instantiasi banyak objek"

Saya mengutip:

"Kamu ingin menggunakannya __slots__jika kamu akan membuat banyak (ratusan, ribuan) objek dari kelas yang sama."

Abstrak Kelas Dasar, misalnya, dari collectionsmodul, tidak dipakai, tetapi __slots__dideklarasikan untuk mereka.

Mengapa?

Jika pengguna ingin menolak __dict__atau membuat __weakref__, hal-hal itu tidak harus tersedia di kelas induk.

__slots__ berkontribusi terhadap usabilitas ulang ketika membuat antarmuka atau mixin.

Memang benar bahwa banyak pengguna Python tidak menulis untuk digunakan kembali, tetapi ketika Anda, memiliki opsi untuk menolak penggunaan ruang yang tidak perlu sangat berharga.

__slots__ tidak merusak acar

Saat memilih objek yang ditempatkan, Anda mungkin akan mengeluhkannya dengan menyesatkan TypeError:

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

Ini sebenarnya salah. Pesan ini berasal dari protokol terlama, yang merupakan default. Anda dapat memilih protokol terbaru dengan -1argumen. Dalam Python 2.7 ini akan menjadi 2(yang diperkenalkan pada 2.3), dan dalam 3.6 itu 4.

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

dalam Python 2.7:

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

dalam Python 3.6

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

Jadi saya akan mengingat ini, karena ini adalah masalah yang terpecahkan.

Kritik atas (hingga 2 Oktober 2016) menerima jawaban

Paragraf pertama adalah setengah penjelasan singkat, setengah prediksi. Inilah satu-satunya bagian yang benar-benar menjawab pertanyaan itu

Penggunaan yang tepat __slots__adalah untuk menghemat ruang pada objek. Alih-alih memiliki dict dinamis yang memungkinkan menambahkan atribut ke objek kapan saja, ada struktur statis yang tidak memungkinkan penambahan setelah pembuatan. Ini menghemat overhead satu dict untuk setiap objek yang menggunakan slot

Babak kedua adalah angan-angan, dan melenceng:

Meskipun ini kadang-kadang optimasi yang berguna, itu akan sama sekali tidak perlu jika juru bahasa Python cukup dinamis sehingga hanya akan membutuhkan dict ketika sebenarnya ada penambahan objek.

Python sebenarnya melakukan sesuatu yang mirip dengan ini, hanya membuat __dict__ketika diakses, tetapi membuat banyak objek tanpa data cukup konyol.

Paragraf kedua terlalu menyederhanakan dan melewatkan alasan sebenarnya untuk dihindari __slots__. Di bawah ini bukan alasan sebenarnya untuk menghindari slot (untuk alasan yang sebenarnya , lihat sisa jawaban saya di atas.):

Mereka mengubah perilaku objek yang memiliki slot dengan cara yang dapat disalahgunakan oleh orang aneh kontrol dan alat pengetik statis.

Kemudian dilanjutkan dengan membahas cara-cara lain untuk mencapai tujuan jahat itu dengan Python, tidak membahas apa pun yang berhubungan dengan __slots__.

Paragraf ketiga adalah angan-angan. Bersama-sama sebagian besar konten yang tidak pantas bahwa penjawab bahkan tidak menulis dan berkontribusi untuk amunisi untuk kritik dari situs.

Bukti penggunaan memori

Buat beberapa objek normal dan objek berlubang:

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

Buat sejuta dari mereka:

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

Periksa dengan 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
...

Akses benda-benda biasa dan benda-benda itu __dict__dan periksa lagi:

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

Ini konsisten dengan sejarah Python, dari tipe dan kelas Unifying di Python 2.2

Jika Anda mensubklasifikasikan tipe bawaan, ruang tambahan ditambahkan secara otomatis ke instance untuk menampung __dict__dan __weakrefs__. (Meskipun __dict__tidak diinisialisasi sampai Anda menggunakannya, jadi Anda tidak perlu khawatir tentang ruang yang ditempati oleh kamus kosong untuk setiap contoh yang Anda buat.) Jika Anda tidak membutuhkan ruang tambahan ini, Anda dapat menambahkan frasa " __slots__ = []" ke kelasmu.

Aaron Hall
sumber
14
wow, satu jawaban yang luar biasa - terima kasih! Namun, saya tidak menangkap class Child(BaseA, BaseB): __slots__ = ('a', 'b')contoh dengan orang tua slot-empy. Mengapa di sini dictproxydibuat alih-alih menaikkan AttributeErroruntuk c?
Skandix
@ Skandix terima kasih telah membawa kesalahan ketik itu menjadi perhatian saya, ternyata c bukan instantiation, saya mungkin lupa saya mengedit bagian itu ketika saya menyimpannya ke dalam sejarah postingan. Mungkin akan tertangkap lebih cepat jika saya melakukan hal yang benar dan membuat kode lebih copy-paste ... Terima kasih lagi!
Aaron Hall
38
Jawaban ini harus menjadi bagian dari dokumentasi Python resmi tentang __slots__. Serius! Terima kasih!
NightElfik
13
@NightElfik percaya atau tidak, saya berkontribusi pada dokumen Python __slots__sekitar setahun yang lalu: github.com/python/cpython/pull/1819/files
Aaron Hall
Jawaban terinci yang fantastis. Saya punya satu pertanyaan: haruskah seseorang menggunakan slot sebagai default kecuali penggunaannya menyentuh salah satu peringatan, atau apakah slot adalah sesuatu yang perlu dipertimbangkan jika Anda tahu Anda akan berjuang untuk kecepatan / memori? Dengan kata lain, haruskah Anda mendorong pemula untuk belajar tentang mereka dan menggunakannya dari awal?
freethebees
265

Mengutip Jacob Hallen :

Penggunaan yang tepat __slots__adalah untuk menghemat ruang pada objek. Alih-alih memiliki dict dinamis yang memungkinkan menambahkan atribut ke objek kapan saja, ada struktur statis yang tidak memungkinkan penambahan setelah pembuatan. [Penggunaan ini __slots__menghilangkan overhead satu dict untuk setiap objek.] Meskipun ini kadang-kadang optimasi yang berguna, itu akan sama sekali tidak perlu jika penerjemah Python cukup dinamis sehingga hanya akan membutuhkan dict ketika sebenarnya ada penambahan ke obyek.

Sayangnya ada efek samping pada slot. Mereka mengubah perilaku objek yang memiliki slot dengan cara yang dapat disalahgunakan oleh orang aneh kontrol dan alat pengetik statis. Ini buruk, karena orang-orang aneh yang mengontrol harus menyalahgunakan metaclasses dan cara mengetik yang statis harus menyalahgunakan dekorator, karena dengan Python, seharusnya hanya ada satu cara yang jelas untuk melakukan sesuatu.

Membuat CPython cukup pintar untuk menangani penghematan ruang tanpa __slots__adalah upaya besar, yang mungkin mengapa itu tidak ada dalam daftar perubahan untuk P3k (belum).

Jeff Bauer
sumber
86
Saya ingin melihat elaborasi pada titik "pengetik statis" / dekorator, sans meremehkan. Mengutip tidak ada pihak ketiga tidak membantu. __slots__tidak mengatasi masalah yang sama dengan mengetik statis. Sebagai contoh, dalam C ++, itu bukan deklarasi variabel anggota sedang dibatasi, itu adalah penugasan jenis yang tidak diinginkan (dan kompiler ditegakkan) untuk variabel itu. Saya tidak memaafkan penggunaan __slots__, hanya tertarik pada percakapan. Terima kasih!
hiwaylon
126

Anda ingin menggunakan __slots__jika Anda akan instantiate banyak (ratusan, ribuan) objek dari kelas yang sama. __slots__hanya ada sebagai alat optimisasi memori.

Sangat tidak disarankan untuk digunakan __slots__untuk membatasi pembuatan atribut.

Objek pengawetan dengan __slots__tidak akan bekerja dengan protokol acar default (terlama); perlu untuk menentukan versi yang lebih baru.

Beberapa fitur introspeksi lain dari python juga dapat terpengaruh.

Ryan
sumber
10
Saya mendemonstrasikan pengawetan objek yang ditempatkan di jawaban saya dan juga membahas bagian pertama dari jawaban Anda.
Aaron Hall
2
Saya mengerti maksud Anda, tetapi slot juga menawarkan akses atribut yang lebih cepat (seperti yang dinyatakan orang lain). Dalam hal ini Anda tidak perlu "untuk instantiate banyak (ratusan, ribuan) objek dari kelas yang sama" untuk mendapatkan kinerja. Yang Anda butuhkan adalah banyak akses ke atribut (slotted) yang sama dari instance yang sama. (Tolong koreksi saya jika saya salah.)
Rotareti
61

Setiap objek python memiliki __dict__atribut yang merupakan kamus yang berisi semua atribut lainnya. misal ketika Anda mengetik self.attrpython sebenarnya sedang melakukan self.__dict__['attr']. Seperti yang dapat Anda bayangkan menggunakan atribut dictionary to store membutuhkan ruang & waktu ekstra untuk mengaksesnya.

Namun, ketika Anda menggunakan __slots__, objek apa pun yang dibuat untuk kelas itu tidak akan memiliki __dict__atribut. Sebagai gantinya, semua akses atribut dilakukan secara langsung melalui pointer.

Jadi, jika menginginkan struktur gaya C daripada kelas penuh yang dapat Anda gunakan __slots__untuk memadatkan ukuran objek & mengurangi waktu akses atribut. Contoh yang baik adalah kelas Point yang berisi atribut x & y. Jika Anda memiliki banyak poin, Anda dapat mencoba menggunakan __slots__untuk menghemat beberapa memori.

Suraj
sumber
10
Tidak, turunan dari kelas dengan __slots__didefinisikan tidak seperti struktur gaya-C. Ada nama atribut pemetaan peta tingkat kamus untuk indeks, kalau tidak, berikut ini tidak akan mungkin: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Saya benar-benar berpikir jawaban ini harus diklarifikasi (saya bisa melakukannya jika Anda mau). Juga, saya tidak yakin itu instance.__hidden_attributes[instance.__class__[attrname]]lebih cepat dari instance.__dict__[attrname].
tzot
22

Selain jawaban lain, berikut adalah contoh penggunaan __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']

Jadi, untuk mengimplementasikannya __slots__, itu hanya membutuhkan garis tambahan (dan membuat kelas Anda menjadi kelas gaya baru jika belum). Dengan cara ini Anda dapat mengurangi jejak memori kelas-kelas tersebut 5 kali lipat , dengan mengorbankan harus menulis kode acar khusus, jika dan ketika itu menjadi perlu.

Evgeni Sergeev
sumber
11

Slot sangat berguna untuk panggilan pustaka untuk menghilangkan "metode pengiriman bernama" saat melakukan panggilan fungsi. Ini disebutkan dalam dokumentasi SWIG . Untuk perpustakaan berkinerja tinggi yang ingin mengurangi fungsi overhead untuk fungsi yang biasa disebut menggunakan slot jauh lebih cepat.

Sekarang ini mungkin tidak berhubungan langsung dengan pertanyaan OP. Ini lebih terkait dengan membangun ekstensi daripada menggunakan sintaks slot pada objek. Tapi itu membantu melengkapi gambar untuk penggunaan slot dan beberapa alasan di baliknya.

Demolishun
sumber
7

Atribut instance kelas memiliki 3 properti: instance, nama atribut, dan nilai atribut.

Dalam akses atribut biasa , instance bertindak sebagai kamus dan nama atribut bertindak sebagai kunci dalam kamus yang mencari nilai.

instance (atribut) -> nilai

Dalam akses __slots__ , nama atribut bertindak sebagai kamus dan instance bertindak sebagai kunci dalam kamus mencari nilai.

atribut (instance) -> nilai

Dalam pola kelas terbang , nama atribut bertindak sebagai kamus dan nilainya bertindak sebagai kunci dalam kamus yang mencari contoh.

atribut (nilai) -> instance

Dmitry Rubanovich
sumber
Ini adalah bagian yang baik, dan tidak akan cocok dengan komentar pada salah satu jawaban yang juga menyarankan bobot terbang, tetapi itu bukan jawaban yang lengkap untuk pertanyaan itu sendiri. Khususnya (hanya dalam konteks pertanyaan): mengapa Flyweight, dan "kasus apa yang harus dihindari ..." __slots__?
Merlyn Morgan-Graham
@Merlyn Morgan-Graham, ini berfungsi sebagai petunjuk untuk memilih: akses reguler, __slots__, atau kelas terbang.
Dmitry Rubanovich
3

Contoh __slot__atribut yang sangat sederhana .

Masalah: Tanpa __slots__

Jika saya tidak memiliki __slot__atribut di kelas saya, saya bisa menambahkan atribut baru ke objek saya.

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}

Jika Anda melihat contoh di atas, Anda dapat melihat bahwa obj1 dan obj2 memiliki atribut x dan y sendiri dan python juga telah membuat dictatribut untuk setiap objek ( obj1 dan obj2 ).

Misalkan jika Tes kelas saya memiliki ribuan objek seperti itu? Membuat atribut tambahan dictuntuk setiap objek akan menyebabkan banyak overhead (memori, daya komputasi dll) dalam kode saya.

Solusi: Dengan __slots__

Sekarang dalam contoh berikut Tes kelas saya berisi __slots__atribut. Sekarang saya tidak bisa menambahkan atribut baru ke objek saya (kecuali atribut x) dan python tidak membuat dictatribut lagi. Ini menghilangkan overhead untuk setiap objek, yang dapat menjadi signifikan jika Anda memiliki banyak objek.

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

Penggunaan lain yang agak kabur __slots__adalah untuk menambahkan atribut ke objek proxy dari paket ProxyTypes, yang sebelumnya merupakan bagian dari proyek PEAK. Ini ObjectWrappermemungkinkan Anda untuk mem-proxy objek lain, tetapi mencegat semua interaksi dengan objek yang diproksikan. Ini tidak terlalu umum digunakan (dan tidak ada dukungan Python 3), tetapi kami telah menggunakannya untuk mengimplementasikan pembungkus pemblokir aman-benang di sekitar implementasi async berdasarkan tornado yang memantulkan semua akses ke objek yang diproksikan melalui ioloop, menggunakan thread-safe concurrent.Futureobjek untuk menyinkronkan dan mengembalikan hasil.

Secara default, akses atribut apa pun ke objek proxy akan memberi Anda hasil dari objek proksi. Jika Anda perlu menambahkan atribut pada objek proxy, __slots__dapat digunakan.

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

Anda telah - pada dasarnya - tidak ada gunanya __slots__.

Untuk saat Anda pikir Anda mungkin perlu __slots__, Anda sebenarnya ingin menggunakan pola desain Ringan atau Terbang . Ini adalah kasus ketika Anda tidak lagi ingin menggunakan objek Python murni. Sebagai gantinya, Anda ingin pembungkus seperti objek Python di sekitar array, struct, atau array numpy.

class Flyweight(object):

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

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

Wrapper seperti kelas tidak memiliki atribut - itu hanya menyediakan metode yang bertindak pada data yang mendasarinya. Metode dapat direduksi menjadi metode kelas. Memang, itu bisa dikurangi menjadi hanya fungsi yang beroperasi pada array data yang mendasarinya.

S.Lott
sumber
17
Apa hubungannya Flyweight __slots__?
oefe
3
@efe: Saya tentu tidak mendapatkan pertanyaan Anda. Saya dapat mengutip jawaban saya, jika itu membantu "ketika Anda berpikir Anda mungkin perlu slot , Anda benar-benar ingin menggunakan ... pola desain kelas terbang". Itulah yang harus dilakukan Flyweight dengan slot . Apakah Anda memiliki pertanyaan yang lebih spesifik?
S.Lott
21
@efe: Flyweight dan __slots__keduanya adalah teknik pengoptimalan untuk menghemat memori. __slots__menunjukkan manfaat ketika Anda memiliki banyak banyak objek serta pola desain Flyweight. Keduanya memecahkan masalah yang sama.
jfs
7
Apakah ada perbandingan yang tersedia antara menggunakan slot dan menggunakan Flyweight mengenai konsumsi memori dan kecepatan?
kontulai
8
Meskipun Flyweight tentu berguna dalam beberapa konteks, percaya atau tidak, jawaban untuk "bagaimana saya bisa mengurangi penggunaan memori dalam Python ketika saya membuat banyak objek" tidak selalu "tidak menggunakan Python untuk banyak objek Anda." Kadang __slots__- kadang benar-benar jawabannya, dan seperti yang ditunjukkan Evgeni, itu dapat ditambahkan sebagai renungan sederhana (misalnya Anda dapat fokus pada kebenaran terlebih dahulu, dan kemudian menambahkan kinerja).
Patrick Maupin
0

Pertanyaan aslinya adalah tentang kasus penggunaan umum tidak hanya tentang memori. Jadi harus disebutkan di sini bahwa Anda juga mendapatkan kinerja yang lebih baik ketika membuat instance objek dalam jumlah besar - menarik misalnya ketika mem-parsing dokumen besar menjadi objek atau dari database.

Berikut ini adalah perbandingan membuat pohon objek dengan sejuta entri, menggunakan slot dan tanpa slot. Sebagai referensi juga kinerja saat menggunakan dicts biasa untuk pohon-pohon (Py2.7.10 pada 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

Kelas uji (ident, appart dari slot):

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)

testcode, mode verbose:

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
Pil Merah
sumber