Python__slots__詳解

来源:http://www.cnblogs.com/rainfd/archive/2017/04/07/slots.html
-Advertisement-
Play Games

當一個類需要創建大量實例時,可以通過`__slots__`聲明實例所需要的屬性, 例如,`class Foo(object): __slots__ = ['foo']`。這樣做帶來以下優點: 1. 更快的屬性訪問速度 2. 減少記憶體消耗 ...


摘要

當一個類需要創建大量實例時,可以通過__slots__聲明實例所需要的屬性,

例如,class Foo(object): __slots__ = ['foo']。這樣做帶來以下優點:

  1. 更快的屬性訪問速度
  2. 減少記憶體消耗

以下測試環境為Ubuntu16.04 Python2.7


Slots的實現

我們首先來看看用純Python是如何實現__slots__(為了將以下實現的slots與原slots區分開來,代碼中用單下劃線的_slots_來代替)

class Member(object):
    # 定義描述器實現slots屬性的查找
    def __init__(self, i):
        self.i = i
    def __get__(self, obj, type=None):
        return obj._slotvalues[self.i]
    def __set__(self, obj, value):
        obj._slotvalues[self.i] = value
        
class Type(type):
    # 使用元類實現slots
    def __new__(self, name, bases,  namespace):
        slots = namespace.get('_slots_')
        if slots:
            for i, slot in enumerate(slots):
                namespace[slot] = Member(i)
            original_init = namespace.get('__init__')
            def __init__(self, *args, **kwargs):
                # 創建_slotvalues列表和調用原來的__init__
                self._slotvalues = [None] * len(slots)
                if original_init(self, *args, **kwargs):
                    original_init(self, *args, **kwargs)
            namespace['__init__'] = __init__
        return type.__new__(self, name, bases, namespace)
    
# Python2與Python3使用元類的區別    
try:
    class Object(object): __metaclass__ = Type
except:
    class Object(metaclass=Type): pass

class A(Object):
    _slots_ = 'x', 'y'

a = A()
a.x = 10
print(a.x)

在CPython中,當一個A類定義了__slots__ = ('x', 'y')A.x就是一個有__get____set__方法的member_descriptor,並且在每個實例中可以通過直接訪問記憶體(direct memory access)獲得。(具體實現是用偏移地址來記錄描述器,通過公式可以直接計算出其在記憶體中的實際地址 ,訪問__dict__也是用相同的方法,也就是說訪問A.__dict__A.x描述器的速度是相近的)

在上面的例子中,我們用純Python實現了一個等價的slots。當一個元類看到_slots_定義了x和y,它會創建兩個的類變數,x = Member(0)y = Member(1)。然後,裝飾__init__方法讓新的實例創建一個_slotvalues列表。

例子中的實現和CPython不同的是:

  • 例子中_slotvalues是一個存儲在類對象外部的列表,而在CPython中它與實例對象存儲在一起,可以通過直接訪問記憶體獲得。相應地,member decriptor也不是存在外部列表中,而同樣可以通過直接訪問記憶體獲得。

  • 預設情況下,__new__方法會為每個實例創建一個字典__dict__來存儲實例的屬性。但如果定義了__slots____new__方法就不會再創建這個字典。

  • 由於不存在__dict__來存儲新的屬性,所以使用一個不在__slots__中的屬性時,程式會報錯。

>>> class A(object): __slots__ = ('x')
>>> a = A()
>>> a.y = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Attribute: 'A' object has no attribute 'y'

可以利用這種特性來限制實例的屬性。


更快的屬性訪問速度

預設情況下,訪問一個實例的屬性是通過訪問該實例的__dict__來實現的。如訪問a.x就相當於訪問a.__dict__['x']。為了便於理解,我粗略地將它拆分為四步:

  1. a.x 2. a.__dict__ 3. a.__dict__['x'] 4. 結果

__slots__的實現可以得知,定義了__slots__的類會為每個屬性創建一個描述器。訪問屬性時就直接調用這個描述器。在這裡我將它拆分為三步:

  1. b.x 2. member decriptor 3. 結果

我在上文提到,訪問__dict__和描述器的速度是相近的,而通過__dict__訪問屬性多了a.__dict__['x']字典訪值一步(一個哈希函數的消耗)。由此可以推斷出,使用了__slots__的類的屬性訪問速度比沒有使用的要快。下麵用一個例子驗證:

from timeit import repeat

class A(object): pass

class B(object): __slots__ = ('x')

def get_set_del_fn(obj):
    def get_set_del():
        obj.x = 1
        obj.x
        del obj.x
    return get_set_del

a = A()
b = B()
ta = min(repeat(get_set_del_fn(a)))
tb = min(repeat(get_set_del_fn(b)))
print("%.2f%%" % ((ta/tb - 1)*100))

在本人電腦上測試速度有0-20%左右的提升。


減少記憶體消耗

Python內置的字典本質是一個哈希表,它是一種用空間換時間的數據結構。為瞭解決衝突的問題,當字典使用量超過2/3時,Python會根據情況進行2-4倍的擴容。由此可預見,取消__dict__的使用可以大幅減少實例的空間消耗。

下麵用pympler模塊測試在不同屬性數目下,使用__slots__前後單個實例占用記憶體大小:

from string import ascii_letters
from pympler.asizeof import asizesof

def slots_memory(num=0):
    attrs = list(ascii_letters[:num])
    class Unslotted(object): pass
    class Slotted(object): __slots__ = attrs
    unslotted = Unslotted()
    slotted = Slotter()
    for attr in attrs:
        unslotted.__dict__[attr] = 0
        exec('slotted.%s = 0' % attr, globals(), locals())
    memory_use = asizesof(slotted, unslotted, unslotted.__dict__)
    return memory_use

def slots_test(nums):
    return [slots_memory(num) for num in nums]

測試結果:(單位:位元組)

屬性數量 slotted unslotted(__dict__)
0 80 334(280)
1 152 408(344)
2 168 448(384)
8 264 1456(1392)
16 392 1776(1712)
25 536 4440(4376)

從上述結果可看到使用__slots__能極大地減少記憶體空間的消耗,這也是最常見到的用法。


使用筆記

1. 只有非字元串的迭代器可以賦值給__slots__

>>> class A(object): __slots__ = ('a', 'b', 'c')
>>> class B(object): __slots__ = 'abcd'
>>> B.__slots__
'abc'

若直接將字元串賦值給它,就只有一個屬性。

2. 關於slots的繼承問題

在一般情況下,使用slots的類需要直接繼承object,如class Foo(object): __slots__ = ()

在繼承自己創建的類時,我根據子類父類是否定義了__slots__,將它細分為六種情況:

  1. 父類有,子類沒有:
    子類的實例還是會自動創建__dict__來存儲屬性,不過父類__slots__已有的屬性不受影響。

    >>> class Father(object): __slots__ = ('x')
    >>> class Son(Base): pass
    >>> son = Son()
    >>> son.x, son.y = 1, 1
    >>> son.__dict__
    >>> {'y': 1}
  2. 父類沒有,子類有:
    雖然子類取消了__dict__,但繼承父類後它會繼續生成。同上面一樣,__slots__已有的屬性不受影響。

    >>> class Father(object): pass
    >>> class Son(Father): __slots__ = ('x')
    >>> son = Son()
    >>> son.x, son.y = 1, 1
    >>> son.__dict__
    >>> {'y': 1}
  3. 父類有,子類有:
    只有子類的__slots__有效,訪問父類有子類沒有的屬性依然會報錯。

    >>> class Father(object): __slots__ = ('x', 'y')
    >>> class Son(Father): __slots__ = ('x', 'z')
    >>> son = Son()
    >>> son.x, son.y, son.z = 1, 1, 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Son' object has no attribute 'y'
  4. 多個擁有非空slots的父類:
    由於__slots__的實現不是簡單的列表或字典,多個父類的非空__slots__不能直接合併,所以使用時會報錯(即使多個父類的非空__slots__是相同的)。

    >>> class Father(object): __slots__ = ('x')
    >>> class Mother(object): __slots__ = ('x')
    >>> class Son(Father, Mother): 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
  5. 多個空slots的父類:
    這是關於slots使用多繼承唯一辦法。

  6. 某些父類有,某些父類沒有:
    跟第一種情況類似。

小結:為了正確使用__slots__,最好直接繼承object。如有需要用到其他父類,則父類和子類都要定義slots,還要記得子類的slots會覆蓋父類的slots。
除非所有父類的slots都為空,否則不要使用多繼承。

3. 添加__dict__獲取動態特性

在特殊情況下,可以在__slots__里添加__dict__來獲取與普通實例同樣的動態特性。

>>> class A(object): __slots__ = ()
>>> class B(A): __slots__ = ('__dict__', 'x')
>>> b = B()
>>> b.x, b.y = 1, 1
>>> b.__dict__
{'y': 1}

4. 添加__weakref__獲取弱引用功能

__slots__的實現不僅取消了__dict__的生成,也取消了__weakref__的生成。同樣的,在__slots__將其添加可以重新獲取弱引用這一功能。

5. 不能通過類屬性給實例設定預設值

定義了__slots__後,這個類的類屬性都變為了描述器。如果給類屬性賦值,就會把描述器給覆蓋了。

6. namedtuple

利用內置的namedtuple不可變的特性,結合slots,能創建出一個輕量不可變的實例。(約等於一個元組的大小)

>>> from collections import namedtuple
>>> class MyNt(namedtupele('MyNt', 'bar baz')): __slots__ = ()
>>> nt = MyNt('r', 'z')
>>> nt.bar
'r'
>>> nt.baz
'z'

總結

當一個類需要創建大量實例時,可以使用__slots__來減少記憶體消耗。如果對訪問屬性的速度有要求,也可以酌情使用。另外可以利用slots的特性來限制實例的屬性。而用在普通類身上時,使用__slots__後會喪失動態添加屬性和弱引用的功能,進而引起其他錯誤,所以在一般情況下不要使用它。

參考資料

Usage of slots?

How slots are implemented


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • MyBatis攔截器原理探究 http://www.cnblogs.com/fangjian0423/p/mybatis-interceptor.html 【myBatis】Mybatis中的攔截器 http://blog.csdn.net/moshenglv/article/details/526 ...
  • 數組:鍵值對組成的語言結構, 根據維數可分為:一維數組、多維數組 根據Key可分為:索引數組、關聯數組 PHP數組 創立數組的方式: $ar_1 = array();//空數組 $ar_2 = array(0,1,2)//初始化數組,有3個值 $ar_3[0] = 1; $ar_3['name'] ...
  • 題目:將一個正整數分解質因數。例如:輸入90,列印輸出90=2*3*3*5。 ...
  • 設計思想: 1.使用正則表達式判斷版本號格式是否正確 2.將字元串用”.”分隔成數組 3.比較數組長度,將長度短的數組用“0”補齊成相等長度數組 4.逐個遍曆數組元素,比較大小 測試用例: 1.版本號為空 2.版本號含非數字 3.版本號長度不一致 4.版本號以點為分隔,逐位比較 ...
  • 本文章轉自: http://blog.csdn.net/cclovett/article/details/12448843 結論:Pattern與Matcher一起合作.Matcher類提供了對正則表達式的分組支持,以及對正則表達式的多次匹配支持. 單獨用Pattern只能使用Pattern.mat ...
  • #coding=utf-8 import sysreload(sys)sys.setdefaultencoding('utf-8') import xlrdimport jiebaimport codecsimport csvimport numpy as npfrom wordcloud impo ...
  • 做基礎的留言板功能 需要三張表: 員工表,留言表,好友表 首先造一個登入頁面: 上圖: 然後來寫處理頁面: 正常的處理登入的頁面只不過把賬號存了一下session 登入上進入主頁面 再來是主頁面了: 查找的條件即是只查自己的好友或者是all的所有人 還要註意一點便是要把調用的db設為全局變數 圖: ...
  • yii2模型的驗證規則,簡單的使用我就不詳細說了,想看的可以去看官網教程http://www.yiichina.com/doc/guide/2.0/structure-models#validation-rules社區網友教程http://www.yiichina.com/topic/6420這裡我 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...