Cách sử dụng __slots__?


Câu trả lời:


1019

Trong Python, mục đích của __slots__nó là gì và những trường hợp nào người ta nên tránh điều này?

TLDR:

Thuộc tính đặc biệt __slots__cho phép bạn nêu rõ các thuộc tính thể hiện mà bạn mong muốn các đối tượng của bạn có, với kết quả mong đợi:

  1. truy cập thuộc tính nhanh hơn .
  2. tiết kiệm không gian trong bộ nhớ.

Tiết kiệm không gian là từ

  1. Lưu trữ giá trị tham chiếu trong các khe thay vì __dict__.
  2. Từ chối __dict____weakref__tạo nếu lớp cha mẹ từ chối chúng và bạn khai báo __slots__.

Hãy cẩn thận

Hãy cẩn thận, bạn chỉ nên khai báo một vị trí cụ thể một lần trong cây thừa kế. Ví dụ:

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

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

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

Python không phản đối khi bạn gặp lỗi này (có lẽ nên như vậy), các vấn đề có thể không biểu hiện khác, nhưng các đối tượng của bạn sẽ chiếm nhiều không gian hơn so với bình thường. Con trăn 3,8:

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

Điều này là do bộ mô tả vị trí của Base có một vị trí tách biệt với phần sai. Điều này thường không xuất hiện, nhưng nó có thể:

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

Sự cảnh báo lớn nhất là dành cho nhiều kế thừa - nhiều "lớp cha với các khe không trống" không thể được kết hợp.

Để phù hợp với hạn chế này, hãy thực hiện theo các thực tiễn tốt nhất: Yếu tố tất cả trừ một hoặc tất cả sự trừu tượng của cha mẹ mà lớp cụ thể của chúng tương ứng và lớp bê tông mới của bạn sẽ kế thừa từ - đưa ra các khe trống trừu tượng (giống như các lớp cơ sở trừu tượng trong thư viện chuẩn).

Xem phần về nhiều kế thừa dưới đây để biết ví dụ.

Yêu cầu:

  • Để có các thuộc tính được đặt tên __slots__thực sự được lưu trữ trong các vị trí thay vì a __dict__, một lớp phải kế thừa từ đó object.

  • Để ngăn chặn việc tạo a __dict__, bạn phải kế thừa objectvà tất cả các lớp trong thừa kế phải khai báo __slots__và không ai trong số chúng có thể có một '__dict__'mục.

Có rất nhiều chi tiết nếu bạn muốn tiếp tục đọc.

Tại sao sử dụng __slots__: Truy cập thuộc tính nhanh hơn.

Người tạo ra Python, Guido van Rossum, nói rằng ông thực sự đã tạo ra __slots__để truy cập thuộc tính nhanh hơn.

Thật là tầm thường khi chứng minh truy cập nhanh hơn đáng kể:

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

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

Truy cập có rãnh nhanh hơn gần 30% trong Python 3.5 trên Ubuntu.

>>> 0.3664822799983085 / 0.2846834529991611
1.2873325658284342

Trong Python 2 trên Windows tôi đã đo nó nhanh hơn khoảng 15%.

Tại sao nên sử dụng __slots__: Tiết kiệm bộ nhớ

Một mục đích khác __slots__là để giảm dung lượng trong bộ nhớ mà mỗi đối tượng chiếm.

Đóng góp của riêng tôi vào tài liệu nêu rõ lý do đằng sau điều này :

Không gian lưu trên sử dụng __dict__có thể là đáng kể.

SQLAlchemy quy rất nhiều bộ nhớ tiết kiệm __slots__.

Để xác minh điều này, bằng cách sử dụng bản phân phối Anaconda của Python 2.7 trên Ubuntu Linux, với guppy.hpy(hay còn gọi là heapy) và sys.getsizeof, kích thước của một thể hiện lớp mà không được __slots__khai báo, và không có gì khác, là 64 byte. Điều đó không bao gồm __dict__. Cảm ơn Python vì đã đánh giá lười biếng một lần nữa, __dict__rõ ràng là không được gọi vào sự tồn tại cho đến khi nó được tham chiếu, nhưng các lớp không có dữ liệu thường là vô dụng. Khi được gọi vào sự tồn tại, __dict__thuộc tính tối thiểu là 280 byte bổ sung.

Ngược lại, một thể hiện lớp với __slots__khai báo là ()(không có dữ liệu) chỉ có 16 byte và 56 byte tổng với một mục trong các khe, 64 với hai.

Đối với Python 64 bit, tôi minh họa mức tiêu thụ bộ nhớ theo byte trong Python 2.7 và 3.6, cho __slots____dict__(không có vị trí được xác định) cho mỗi điểm mà dict tăng lên trong 3.6 (ngoại trừ các thuộc tính 0, 1 và 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

Vì vậy, mặc dù có các đoạn nhỏ hơn trong Python 3, chúng tôi thấy __slots__các trường hợp độc đáo để tiết kiệm bộ nhớ cho chúng tôi và đó là một lý do chính mà bạn muốn sử dụng __slots__.

Chỉ để hoàn thiện các ghi chú của tôi, lưu ý rằng có một chi phí một lần cho mỗi vị trí trong không gian tên của lớp là 64 byte trong Python 2 và 72 byte trong Python 3, bởi vì các vị trí sử dụng các mô tả dữ liệu như các thuộc tính, được gọi là "thành viên".

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

Trình diễn __slots__:

Để từ chối việc tạo a __dict__, bạn phải phân lớp object:

class Base(object): 
    __slots__ = ()

hiện nay:

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

Hoặc phân lớp một lớp khác định nghĩa __slots__

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

và bây giờ:

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

nhưng:

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

Để cho phép __dict__tạo trong khi phân lớp các đối tượng có rãnh, chỉ cần thêm '__dict__'vào __slots__(lưu ý rằng các vị trí được đặt hàng và bạn không nên lặp lại các vị trí đã có trong các lớp cha):

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

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

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

Hoặc thậm chí bạn không cần phải khai báo __slots__trong lớp con của mình và bạn vẫn sẽ sử dụng các vị trí từ cha mẹ, nhưng không hạn chế việc tạo __dict__:

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

Và:

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

Tuy nhiên, __slots__có thể gây ra sự cố cho nhiều kế thừa:

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

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

Bởi vì việc tạo một lớp con từ cha mẹ với cả hai vị trí không trống: đều thất bại:

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

Nếu bạn gặp phải vấn đề này, Bạn có thể xóa __slots__khỏi cha mẹ hoặc nếu bạn có quyền kiểm soát của cha mẹ, cung cấp cho họ các vị trí trống hoặc cấu trúc lại để trừu tượng hóa:

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!

Thêm '__dict__'vào __slots__để nhận nhiệm vụ động:

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

và bây giờ:

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

Vì vậy, '__dict__'trong các vị trí, chúng tôi mất một số lợi ích về kích thước với mặt trái của việc gán động và vẫn có các vị trí cho tên mà chúng tôi mong đợi.

Khi bạn kế thừa từ một đối tượng không có rãnh, bạn sẽ có cùng một loại ngữ nghĩa khi bạn sử dụng __slots__- các tên được đặt trong __slots__các giá trị có rãnh, trong khi bất kỳ giá trị nào khác được đặt trong trường hợp __dict__.

Tránh __slots__bởi vì bạn muốn có thể thêm các thuộc tính một cách nhanh chóng thực sự không phải là một lý do chính đáng - chỉ cần thêm "__dict__"vào của bạn __slots__nếu điều này là bắt buộc.

Bạn có thể thêm __weakref__vào __slots__một cách rõ ràng nếu bạn cần tính năng đó.

Đặt thành tuple trống khi phân lớp một tên được đặt tên:

Nội dung được đặt tên tạo ra các trường hợp bất biến rất nhẹ (về cơ bản, kích thước của bộ dữ liệu) nhưng để có được lợi ích, bạn cần phải tự làm nếu bạn phân lớp chúng:

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

sử dụng:

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

Và cố gắng gán một thuộc tính bất ngờ làm tăng AttributeErrorvì chúng tôi đã ngăn chặn việc tạo __dict__:

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

Bạn có thể cho phép __dict__tạo bằng cách bỏ đi __slots__ = (), nhưng bạn không thể sử dụng không trống __slots__với các kiểu con của tuple.

Caveat lớn nhất: Nhiều kế thừa

Ngay cả khi các vị trí không trống giống nhau cho nhiều cha mẹ, chúng không thể được sử dụng cùng nhau:

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

Việc sử dụng một khoảng trống __slots__trong cha mẹ dường như mang lại sự linh hoạt nhất, cho phép trẻ lựa chọn ngăn chặn hoặc cho phép (bằng cách thêm '__dict__'để có được sự phân công động, xem phần bên trên) việc tạo ra một__dict__ :

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

Bạn không cần phải có vị trí - vì vậy nếu bạn thêm chúng và xóa chúng sau, nó sẽ không gây ra vấn đề gì.

Đi ra ngoài một chi ở đây : Nếu bạn đang soạn các mixin hoặc sử dụng các lớp cơ sở trừu tượng , không có ý định khởi tạo, thì một khoảng trống __slots__trong các bậc cha mẹ đó dường như là cách tốt nhất để đạt được sự linh hoạt cho các lớp con.

Để chứng minh, trước tiên, hãy tạo một lớp có mã mà chúng tôi muốn sử dụng theo nhiều kế thừa

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

Chúng tôi có thể sử dụng trực tiếp ở trên bằng cách kế thừa và khai báo các vị trí dự kiến:

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

Nhưng chúng tôi không quan tâm đến điều đó, đó là sự thừa kế đơn lẻ tầm thường, chúng tôi cần một lớp khác mà chúng tôi cũng có thể thừa hưởng, có thể với một thuộc tính ồn ào:

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

Bây giờ nếu cả hai căn cứ đều có các khe không trống, chúng tôi không thể thực hiện các thao tác dưới đây. (Trên thực tế, nếu chúng ta muốn, chúng ta có thể đã đưa ra AbstractBasecác vị trí không trống a và b, và để chúng ra khỏi tuyên bố dưới đây - để chúng vào sẽ là sai):

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

Và bây giờ chúng tôi có chức năng từ cả hai thông qua nhiều kế thừa, và vẫn có thể từ chối __dict____weakref__khởi tạo:

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

Các trường hợp khác để tránh khe cắm:

  • Tránh chúng khi bạn muốn thực hiện __class__gán với một lớp khác không có chúng (và bạn không thể thêm chúng) trừ khi bố trí vị trí giống hệt nhau. (Tôi rất thích tìm hiểu ai đang làm việc này và tại sao.)
  • Tránh chúng nếu bạn muốn các lớp con có độ dài thay đổi như lớp dài, tuple hoặc str và bạn muốn thêm thuộc tính cho chúng.
  • Tránh chúng nếu bạn khăng khăng cung cấp các giá trị mặc định thông qua các thuộc tính lớp cho các biến thể hiện.

Bạn có thể trêu chọc thêm từ phần còn lại của __slots__ tài liệu (tài liệu 3.7 dev là mới nhất) , mà tôi đã có những đóng góp đáng kể gần đây.

Phê bình các câu trả lời khác

Các câu trả lời hàng đầu hiện nay trích dẫn thông tin lỗi thời và khá gợn sóng và bỏ lỡ dấu hiệu trong một số cách quan trọng.

Không "chỉ sử dụng __slots__khi khởi tạo nhiều đối tượng"

Tôi trích dẫn:

"Bạn sẽ muốn sử dụng __slots__nếu bạn định khởi tạo rất nhiều (hàng trăm, hàng ngàn) đối tượng cùng loại."

Các lớp cơ sở trừu tượng, ví dụ, từ collectionsmô-đun, không được khởi tạo, nhưng __slots__được khai báo cho chúng.

Tại sao?

Nếu người dùng muốn từ chối __dict__hoặc __weakref__tạo, những thứ đó không được có sẵn trong các lớp cha.

__slots__ góp phần tái sử dụng khi tạo giao diện hoặc mixin.

Đúng là nhiều người dùng Python không viết để sử dụng lại, nhưng khi bạn có, tùy chọn từ chối sử dụng không gian không cần thiết là có giá trị.

__slots__ không phá vỡ dưa chua

Khi tẩy một vật có rãnh, bạn có thể thấy nó phàn nàn với sự sai lệch TypeError:

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

Điều này thực sự không chính xác. Thông báo này đến từ giao thức cũ nhất, là mặc định. Bạn có thể chọn giao thức mới nhất với -1đối số. Trong Python 2.7, điều này sẽ là 2(được giới thiệu trong 2.3) và trong 3.6 4.

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

trong Python 2.7:

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

trong Python 3.6

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

Vì vậy, tôi sẽ ghi nhớ điều này, vì nó là một vấn đề được giải quyết.

Phê bình về câu trả lời (cho đến ngày 2 tháng 10 năm 2016)

Đoạn đầu tiên là một nửa giải thích ngắn, một nửa dự đoán. Đây là phần duy nhất thực sự trả lời câu hỏi

Việc sử dụng đúng __slots__là để tiết kiệm không gian trong các đối tượng. Thay vì có một lệnh động cho phép thêm thuộc tính vào các đối tượng bất cứ lúc nào, có một cấu trúc tĩnh không cho phép bổ sung sau khi tạo. Điều này giúp tiết kiệm chi phí của một dict cho mọi đối tượng sử dụng vị trí

Nửa thứ hai là mơ tưởng, và đánh dấu:

Mặc dù điều này đôi khi là một tối ưu hóa hữu ích, nhưng nó sẽ hoàn toàn không cần thiết nếu trình thông dịch Python đủ năng động để nó chỉ yêu cầu lệnh khi thực sự có bổ sung cho đối tượng.

Python thực sự làm một cái gì đó tương tự như thế này, chỉ tạo ra __dict__khi nó được truy cập, nhưng việc tạo ra nhiều đối tượng không có dữ liệu là khá vô lý.

Đoạn thứ hai quá đơn giản và bỏ lỡ các lý do thực tế để tránh __slots__. Dưới đây không phải là lý do thực sự để tránh các vị trí (vì lý do thực tế , hãy xem phần còn lại của câu trả lời của tôi ở trên.):

Chúng thay đổi hành vi của các đối tượng có các vị trí theo cách có thể bị lạm dụng bằng cách kiểm soát các quái vật và các ký hiệu gõ tĩnh.

Sau đó, tiếp tục thảo luận về các cách khác để thực hiện mục tiêu đồi trụy đó với Python, không thảo luận bất cứ điều gì để làm __slots__.

Đoạn thứ ba là mơ tưởng nhiều hơn. Cùng với nhau, chủ yếu là nội dung không chính xác mà người trả lời thậm chí không phải là tác giả và đóng góp đạn dược cho các nhà phê bình của trang web.

Bằng chứng sử dụng bộ nhớ

Tạo một số đối tượng bình thường và các đối tượng có rãnh:

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

Khởi tạo một triệu trong số họ:

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

Kiểm tra với 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
...

Truy cập các đối tượng thông thường và của họ __dict__và kiểm tra lại:

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

Điều này phù hợp với lịch sử của Python, từ các loại và lớp thống nhất trong Python 2.2

Nếu bạn phân lớp một loại tích hợp, không gian thêm sẽ tự động được thêm vào các thể hiện để chứa __dict____weakrefs__. ( __dict__Mặc dù không được khởi tạo cho đến khi bạn sử dụng nó, vì vậy bạn không nên lo lắng về không gian bị chiếm bởi một từ điển trống cho mỗi phiên bản bạn tạo.) Nếu bạn không cần thêm dung lượng này, bạn có thể thêm cụm từ " __slots__ = []" vào lớp của bạn.


14
wow, một địa ngục của một câu trả lời - cảm ơn! Tuy nhiên, tôi đã không bắt được class Child(BaseA, BaseB): __slots__ = ('a', 'b')ví dụ với cha mẹ empy-slot-slot. Tại sao ở đây một dictproxytạo ra thay vì tăng một AttributeErrorcho c?
Skandix

@Skandix cảm ơn vì đã chú ý đến lỗi đánh máy đó, hóa ra c không phải là một bản sao, tôi có thể quên tôi đã chỉnh sửa phần đó khi tôi lưu nó vào lịch sử bài viết. Có lẽ nó đã bị bắt sớm hơn nếu tôi đã làm đúng và làm cho mã dễ sao chép hơn ... Cảm ơn một lần nữa!
Aaron Hall

38
Câu trả lời này phải là một phần của tài liệu Python chính thức về __slots__. Nghiêm túc! Cảm ơn bạn!
NightElfik

13
@NightElfik tin hay không, tôi đã đóng góp cho tài liệu Python vào __slots__khoảng một năm trước: github.com/python/cpython/pull/1819/files
Aaron Hall

Câu trả lời chi tiết tuyệt vời. Tôi có một câu hỏi: một người nên sử dụng các vị trí như mặc định trừ khi việc sử dụng đạt được một trong những cảnh báo hoặc là các vị trí cần xem xét nếu bạn biết bạn sẽ phải vật lộn với tốc độ / bộ nhớ? Nói cách khác, bạn có nên khuyến khích một người mới tìm hiểu về họ và sử dụng chúng ngay từ đầu không?
freethebees

265

Trích dẫn Jacob Hallen :

Việc sử dụng đúng __slots__là để tiết kiệm không gian trong các đối tượng. Thay vì có một lệnh động cho phép thêm thuộc tính vào các đối tượng bất cứ lúc nào, có một cấu trúc tĩnh không cho phép bổ sung sau khi tạo. [Việc sử dụng này __slots__giúp loại bỏ chi phí của một dict cho mọi đối tượng.] Mặc dù đôi khi đây là một tối ưu hóa hữu ích, nhưng sẽ hoàn toàn không cần thiết nếu trình thông dịch Python đủ năng động để nó chỉ yêu cầu dict khi thực sự có bổ sung cho vật.

Thật không may, có một tác dụng phụ cho khe. Chúng thay đổi hành vi của các đối tượng có các vị trí theo cách có thể bị lạm dụng bằng cách kiểm soát các quái vật và các ký hiệu gõ tĩnh. Điều này là xấu, bởi vì những kẻ thích kiểm soát nên lạm dụng các siêu dữ liệu và các thuật ngữ gõ tĩnh nên lạm dụng các trình trang trí, vì trong Python, chỉ nên có một cách rõ ràng để làm một cái gì đó.

Làm cho CPython đủ thông minh để xử lý tiết kiệm không gian mà không phải __slots__là một công việc chính, đó có thể là lý do tại sao nó không nằm trong danh sách các thay đổi cho P3k (chưa).


86
Tôi muốn xem một số chi tiết về điểm "gõ tĩnh" / trang trí, sans pejorative. Trích dẫn bên thứ ba vắng mặt là không có ích. __slots__không giải quyết các vấn đề tương tự như gõ tĩnh. Ví dụ, trong C ++, nó không phải là khai báo của một biến thành viên đang bị hạn chế, đó là việc gán một loại ngoài ý muốn (và trình biên dịch được thi hành) cho biến đó. Tôi không bỏ qua việc sử dụng __slots__, chỉ quan tâm đến cuộc trò chuyện. Cảm ơn!
hiwaylon

126

Bạn sẽ muốn sử dụng __slots__nếu bạn định khởi tạo rất nhiều (hàng trăm, hàng nghìn) đối tượng cùng loại. __slots__chỉ tồn tại như một công cụ tối ưu hóa bộ nhớ.

Nó không được khuyến khích sử dụng __slots__để hạn chế việc tạo thuộc tính.

Pickling đối tượng __slots__không hoạt động với giao thức pickle mặc định (cũ nhất); nó là cần thiết để xác định một phiên bản mới hơn.

Một số tính năng hướng nội khác của trăn cũng có thể bị ảnh hưởng xấu.


10
Tôi chứng minh việc chọn một đối tượng có rãnh trong câu trả lời của mình và cũng giải quyết phần đầu tiên trong câu trả lời của bạn.
Aaron Hall

2
Tôi thấy quan điểm của bạn, nhưng các vị trí cũng cung cấp quyền truy cập thuộc tính nhanh hơn (như những người khác đã nêu). Trong trường hợp đó, bạn không cần "khởi tạo nhiều (hàng trăm, hàng nghìn) đối tượng cùng loại" để đạt được hiệu suất. Thay vào đó, những gì bạn cần là rất nhiều quyền truy cập vào cùng một thuộc tính (có rãnh) của cùng một thể hiện. (Vui lòng sửa lại cho tôi nếu tôi sai.)
Rotareti

61

Mỗi đối tượng python có một __dict__thuộc tính là một từ điển chứa tất cả các thuộc tính khác. ví dụ như khi bạn gõ self.attrpython đang thực sự làm self.__dict__['attr']. Như bạn có thể tưởng tượng, việc sử dụng một từ điển để lưu trữ thuộc tính cần thêm không gian và thời gian để truy cập nó.

Tuy nhiên, khi bạn sử dụng __slots__, bất kỳ đối tượng nào được tạo cho lớp đó sẽ không có __dict__thuộc tính. Thay vào đó, tất cả các truy cập thuộc tính được thực hiện trực tiếp thông qua con trỏ.

Vì vậy, nếu muốn cấu trúc kiểu C thay vì lớp đầy đủ, bạn có thể sử dụng __slots__để nén kích thước của các đối tượng và giảm thời gian truy cập thuộc tính. Một ví dụ điển hình là lớp Point chứa các thuộc tính x & y. Nếu bạn sẽ có rất nhiều điểm, bạn có thể thử sử dụng __slots__để bảo tồn một số bộ nhớ.


10
Không, một thể hiện của một lớp __slots__được định nghĩa không giống như cấu trúc kiểu C. Có một tên thuộc tính ánh xạ từ điển cấp lớp cho các chỉ mục, nếu không thì điều sau đây là không thể: class A(object): __slots__= "value",\n\na=A(); setattr(a, 'value', 1)Tôi thực sự nghĩ rằng câu trả lời này cần được làm rõ (tôi có thể làm điều đó nếu bạn muốn). Ngoài ra, tôi không chắc instance.__hidden_attributes[instance.__class__[attrname]]là nhanh hơn instance.__dict__[attrname].
tzot

22

Ngoài các câu trả lời khác, đây là một ví dụ về việc sử dụng __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']

Vì vậy, để thực hiện __slots__, nó chỉ cần thêm một dòng (và biến lớp của bạn thành một lớp kiểu mới nếu nó chưa có). Bằng cách này, bạn có thể giảm dung lượng bộ nhớ của các lớp đó xuống 5 lần , với chi phí phải viết mã dưa chua tùy chỉnh, nếu và khi điều đó trở nên cần thiết.


11

Slots rất hữu ích cho các cuộc gọi thư viện để loại bỏ "công văn phương thức được đặt tên" khi thực hiện các cuộc gọi hàm. Điều này được đề cập trong tài liệu SWIG . Đối với các thư viện hiệu suất cao muốn giảm chi phí chức năng cho các chức năng thường được gọi là sử dụng vị trí thì nhanh hơn nhiều.

Bây giờ điều này có thể không liên quan trực tiếp đến câu hỏi OP. Nó liên quan nhiều hơn đến việc xây dựng các tiện ích mở rộng hơn là sử dụng cú pháp vị trí trên một đối tượng. Nhưng nó giúp hoàn thành bức tranh cho việc sử dụng các vị trí và một số lý do đằng sau chúng.


7

Một thuộc tính của một thể hiện lớp có 3 thuộc tính: thể hiện, tên của thuộc tính và giá trị của thuộc tính.

Trong truy cập thuộc tính thông thường , thể hiện hoạt động như một từ điển và tên của thuộc tính đóng vai trò là khóa trong từ điển tìm kiếm giá trị.

thể hiện (thuộc tính) -> giá trị

Trong truy cập __slots__ , tên của thuộc tính đóng vai trò là từ điển và thể hiện đóng vai trò là khóa trong từ điển tìm kiếm giá trị.

thuộc tính (ví dụ) -> giá trị

Trong mẫu fly trọng , tên của thuộc tính đóng vai trò là từ điển và giá trị đóng vai trò là khóa trong từ điển đó tìm kiếm thể hiện.

thuộc tính (giá trị) -> thể hiện


Đây là một chia sẻ tốt và sẽ không phù hợp trong một nhận xét về một trong những câu trả lời cũng gợi ý sự bay bổng, nhưng nó không phải là một câu trả lời hoàn chỉnh cho chính câu hỏi. Cụ thể (chỉ trong bối cảnh của câu hỏi): tại sao Flykg và "những trường hợp nào người ta nên tránh ..." __slots__?
Merlyn Morgan-Graham

@Merlyn Morgan-Graham, đây là một gợi ý để chọn: truy cập thường xuyên, __slots__ hoặc cân nặng.
Dmitry Rubanovich

3

Một ví dụ rất đơn giản về __slot__thuộc tính.

Vấn đề: Không có __slots__

Nếu tôi không có __slot__thuộc tính trong lớp, tôi có thể thêm thuộc tính mới vào đối tượng của mình.

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}

Nếu bạn nhìn vào ví dụ trên, bạn có thể thấy obj1obj2 có các thuộc tính xy riêng và python cũng đã tạo một dictthuộc tính cho từng đối tượng ( obj1obj2 ).

Giả sử nếu lớp Kiểm tra của tôi có hàng ngàn đối tượng như vậy? Tạo một thuộc tính bổ sung dictcho mỗi đối tượng sẽ gây ra nhiều chi phí (bộ nhớ, khả năng tính toán, v.v.) trong mã của tôi.

Giải pháp: Với __slots__

Bây giờ trong ví dụ sau đây, lớp Test của tôi chứa __slots__thuộc tính. Bây giờ tôi không thể thêm thuộc tính mới vào các đối tượng của mình (ngoại trừ thuộc tính x) và python không tạo dictthuộc tính nữa. Điều này giúp loại bỏ chi phí cho từng đối tượng, có thể trở nên quan trọng nếu bạn có nhiều đối tượng.

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'

2

Một cách sử dụng hơi khó hiểu khác __slots__là thêm các thuộc tính vào proxy đối tượng từ gói ProxyTypes, trước đây là một phần của dự án PEAK. Nó ObjectWrappercho phép bạn ủy quyền một đối tượng khác, nhưng chặn tất cả các tương tác với đối tượng ủy quyền. Nó không được sử dụng phổ biến (và không hỗ trợ Python 3), nhưng chúng tôi đã sử dụng nó để triển khai trình bao chặn chặn an toàn luồng xung quanh việc triển khai async dựa trên cơn lốc xoáy, trả lại tất cả quyền truy cập vào đối tượng được ủy quyền thông qua ioloop, sử dụng an toàn luồng concurrent.Futurecác đối tượng để đồng bộ hóa và trả về kết quả.

Theo mặc định, bất kỳ quyền truy cập thuộc tính nào vào đối tượng proxy sẽ cung cấp cho bạn kết quả từ đối tượng proxy. Nếu bạn cần thêm một thuộc tính trên đối tượng proxy, __slots__có thể được sử dụng.

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

1

Bạn có - về cơ bản - không sử dụng cho __slots__.

Đối với thời gian mà bạn nghĩ rằng bạn có thể cần __slots__, bạn thực sự muốn sử dụng các mẫu thiết kế Nhẹ hoặc Flykg . Đây là những trường hợp khi bạn không còn muốn sử dụng các đối tượng Python thuần túy. Thay vào đó, bạn muốn một trình bao bọc giống như đối tượng Python xung quanh một mảng, struct hoặc numpy mảng.

class Flyweight(object):

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

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

Trình bao bọc giống như lớp không có thuộc tính - nó chỉ cung cấp các phương thức hoạt động trên dữ liệu cơ bản. Các phương thức có thể được giảm xuống các phương thức lớp. Thật vậy, nó có thể được giảm xuống chỉ còn các chức năng hoạt động trên mảng dữ liệu cơ bản.


17
Flykg có liên quan __slots__gì?
oefe

3
@oefe: Tôi chắc chắn không nhận được câu hỏi của bạn. Tôi có thể trích dẫn câu trả lời của mình, nếu nó giúp "khi bạn nghĩ rằng bạn có thể cần các vị trí , bạn thực sự muốn sử dụng ... Mẫu thiết kế có trọng lượng". Đó là những gì Flykg phải làm với các khe cắm . Bạn có một câu hỏi cụ thể hơn?
S.Lott

21
@oefe: Fly weight và __slots__là cả hai kỹ thuật tối ưu hóa để tiết kiệm bộ nhớ. __slots__cho thấy lợi ích khi bạn có nhiều đối tượng cũng như mẫu thiết kế Flykg. Cả hai giải quyết cùng một vấn đề.
jfs

7
Có sự so sánh khả dụng giữa việc sử dụng các vị trí và sử dụng Flykg về mức tiêu thụ và tốc độ bộ nhớ không?
kontulai

8
Mặc dù Flykg chắc chắn rất hữu ích trong một số bối cảnh, tin hay không, nhưng câu trả lời cho "làm thế nào tôi có thể giảm mức sử dụng bộ nhớ trong Python khi tôi tạo ra một trăm đối tượng" không phải lúc nào cũng "không sử dụng Python cho các đối tượng zillion của bạn." Đôi khi __slots__thực sự là câu trả lời, và như Evgeni chỉ ra, nó có thể được thêm vào như một suy nghĩ đơn giản (ví dụ: bạn có thể tập trung vào tính chính xác trước, sau đó thêm hiệu suất).
Patrick Maupin

0

Câu hỏi ban đầu là về các trường hợp sử dụng chung không chỉ về bộ nhớ. Vì vậy, cần đề cập ở đây rằng bạn cũng có hiệu suất tốt hơn khi khởi tạo một lượng lớn đối tượng - thú vị, ví dụ như khi phân tích tài liệu lớn thành đối tượng hoặc từ cơ sở dữ liệu.

Dưới đây là so sánh về việc tạo các cây đối tượng với một triệu mục, sử dụng các vị trí và không có vị trí. Như một tài liệu tham khảo cũng là hiệu suất khi sử dụng các ký tự đơn giản cho cây (Py2.7.10 trên 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

Các lớp kiểm tra (nhận dạng, appart từ các vị trí):

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)

mã kiểm tra, chế độ dài dòng:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.