1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        你寫的 Python 代碼可以更“瘦”

        共 5780字,需瀏覽 12分鐘

         ·

        2020-09-04 21:47


        作者:intellimath ?譯者 :彎月,責(zé)編:郭芮?出品:CSDN


        在執(zhí)行程序時,如果內(nèi)存中有大量活動的對象,就可能出現(xiàn)內(nèi)存問題,尤其是在可用內(nèi)存總量有限的情況下。在本文中,我們將討論縮小對象的方法,大幅減少 Python 所需的內(nèi)存。


        為了簡便起見,我們以一個表示點的 Python 結(jié)構(gòu)為例,它包括 x、y、z 坐標(biāo)值,坐標(biāo)值可以通過名稱訪問。

        Dict
        在小型程序中,特別是在腳本中,使用 Python 自帶的 dict 來表示結(jié)構(gòu)信息非常簡單方便:
        >>>?ob?=?{'x':1,?'y':2,?'z':3}
        >>>?x?=?ob['x']
        >>>?ob['y']?=?y
        由于在 Python 3.6 中 dict 的實現(xiàn)采用了一組有序鍵,因此其結(jié)構(gòu)更為緊湊,更深得人心。但是,讓我們看看 dict 在內(nèi)容中占用的空間大小:
        >>>?print(sys.getsizeof(ob))
        240


        如上所示,dict 占用了大量內(nèi)存,尤其是如果突然虛需要創(chuàng)建大量實例時:
        實例數(shù)
        對象大小
        1 000 000
        240 Mb
        10 000 000
        2.40 Gb
        100 000 000
        24 Gb

        類實例
        有些人希望將所有東西都封裝到類中,他們更喜歡將結(jié)構(gòu)定義為可以通過屬性名訪問的類:
        class?Point:
        ????#
        ????def?__init__(self,?x,?y,?z):
        ????????self.x?=?x
        ????????self.y?=?y
        ????????self.z?=?z

        >>>?ob?=?Point(1,2,3)
        >>>?x?=?ob.x
        >>>?ob.y?=?y
        類實例的結(jié)構(gòu)很有趣:
        字段
        大?。ū忍兀?/span>
        PyGC_Head
        24
        PyObject_HEAD
        16
        __weakref__
        8
        __dict__
        8
        合計:
        56
        在上表中,__weakref__ 是該列表的引用,稱之為到該對象的弱引用(weak reference);字段 __dict__ 是該類的實例字典的引用,其中包含實例屬性的值(注意在 64-bit 引用平臺中占用 8 字節(jié))。從 Python 3.3 開始,所有類實例的字典的鍵都存儲在共享空間中。這樣就減少了內(nèi)存中實例的大?。?/span>
        >>>?print(sys.getsizeof(ob),?sys.getsizeof(ob.__dict__))?
        56?112
        因此,大量類實例在內(nèi)存中占用的空間少于常規(guī)字典(dict):
        實例數(shù)
        大小
        1 000 000
        168 Mb
        10 000 000
        1.68 Gb
        100 000 000
        16.8 Gb
        不難看出,由于實例的字典很大,所以實例依然占用了大量內(nèi)存。

        帶有 __slots__ 的類實例
        為了大幅降低內(nèi)存中類實例的大小,我們可以考慮干掉 __dict__ 和__weakref__。為此,我們可以借助 __slots__:
        class?Point:
        ????__slots__?=?'x',?'y',?'z'

        ????def?__init__(self,?x,?y,?z):
        ????????self.x?=?x
        ????????self.y?=?y
        ????????self.z?=?z

        >>>?ob?=?Point(1,2,3)
        >>>?print(sys.getsizeof(ob))
        64
        如此一來,內(nèi)存中的對象就明顯變小了:
        字段
        大小(比特)
        PyGC_Head
        24
        PyObject_HEAD
        16
        x
        8
        y
        8
        z
        8
        總計:
        64
        在類的定義中使用了 __slots__ 以后,大量實例占據(jù)的內(nèi)存就明顯減少了:
        實例數(shù)
        大小
        1 000 000
        64 Mb
        10 000 000
        640 Mb
        100 000 000
        6.4 Gb
        目前,這是降低類實例占用內(nèi)存的主要方式。
        這種方式減少內(nèi)存的原理為:在內(nèi)存中,對象的標(biāo)題后面存儲的是對象的引用(即屬性值),訪問這些屬性值可以使用類字典中的特殊描述符:
        >>>?pprint(Point.__dict__)
        mappingproxy(
        ??????????????....................................
        ??????????????'x':?'x'?of?'Point'?objects>,
        ??????????????'y':?'y'?of?'Point'?objects>,
        ??????????????'z':?'z'?of?'Point'?objects>})
        為了自動化使用 __slots__ 創(chuàng)建類的過程,你可以使用庫namedlist(https://pypi.org/project/namedlist)。namedlist.namedlist 函數(shù)可以創(chuàng)建帶有 __slots__ 的類:
        >>>?Point?=?namedlist('Point',?('x',?'y',?'z'))
        還有一個包 attrs(https://pypi.org/project/attrs),無論使用或不使用 __slots__ 都可以利用這個包自動創(chuàng)建類。

        元組
        Python 還有一個自帶的元組(tuple)類型,代表不可修改的數(shù)據(jù)結(jié)構(gòu)。元組是固定的結(jié)構(gòu)或記錄,但它不包含字段名稱。你可以利用字段索引訪問元組的字段。在創(chuàng)建元組實例時,元組的字段會一次性關(guān)聯(lián)到值對象:
        >>>?ob?=?(1,2,3)
        >>>?x?=?ob[0]
        >>>?ob[1]?=?y?#?ERROR
        元組實例非常緊湊:
        >>>?print(sys.getsizeof(ob))
        72
        由于內(nèi)存中的元組還包含字段數(shù),因此需要占據(jù)內(nèi)存的 8 個字節(jié),多于帶有 __slots__ 的類:
        字段
        大?。ㄗ止?jié))
        PyGC_Head
        24
        PyObject_HEAD
        16
        ob_size
        8
        [0]
        8
        [1]
        8
        [2]
        8
        總計:
        72

        命名元組
        由于元組的使用非常廣泛,所以終有一天你需要通過名稱訪問元組。為了滿足這種需求,你可以使用模塊 collections.namedtuple。
        namedtuple 函數(shù)可以自動生成這種類:
        >>>?Point?=?namedtuple('Point',?('x',?'y',?'z'))
        如上代碼創(chuàng)建了元組的子類,其中還定義了通過名稱訪問字段的描述符。對于上述示例,訪問方式如下:
        ?class?Point(tuple):
        ?????#
        ?????@property
        ?????def?_get_x(self):
        ?????????return?self[0]
        ?????@property
        ?????def?_get_y(self):
        ?????????return?self[1]
        ?????@property
        ?????def?_get_z(self):
        ?????????return?self[2]
        ?????#
        ?????def?__new__(cls,?x,?y,?z):
        ?????????return?tuple.__new__(cls,?(x,?y,?z))
        這種類所有的實例所占用的內(nèi)存與元組完全相同。但大量的實例占用的內(nèi)存也會稍稍多一些:
        實例數(shù)
        大小
        1 000 000
        72 Mb
        10 000 000
        720 Mb
        100 000 000
        7.2 Gb

        記錄類:不帶循環(huán) GC 的可變更命名元組
        由于元組及其相應(yīng)的命名元組類能夠生成不可修改的對象,因此類似于 ob.x 的對象值不能再被賦予其他值,所以有時還需要可修改的命名元組。由于 Python 沒有相當(dāng)于元組且支持賦值的內(nèi)置類型,因此人們想了許多辦法。在這里我們討論一下記錄類(recordclass,https://pypi.org/project/recordclass),它在 StackoverFlow 上廣受好評(https://stackoverflow.com/questions/29290359/existence-of-mutable-named-tuple-in)。
        此外,它還可以將對象占用的內(nèi)存量減少到與元組對象差不多的水平。
        recordclass 包引入了類型 recordclass.mutabletuple,它幾乎等價于元組,但它支持賦值。它會創(chuàng)建幾乎與 namedtuple 完全一致的子類,但支持給屬性賦新值(而不需要創(chuàng)建新的實例)。recordclass 函數(shù)與 namedtuple 函數(shù)類似,可以自動創(chuàng)建這些類:
        ?>>>?Point?=?recordclass('Point',?('x',?'y',?'z'))
        ?>>>?ob?=?Point(1,?2,?3)
        類實例的結(jié)構(gòu)也類似于 tuple,但沒有 PyGC_Head:
        字段
        大?。ㄗ止?jié))
        PyObject_HEAD
        16
        ob_size
        8
        x
        8
        y
        8
        z
        8
        總計:
        48
        在默認(rèn)情況下,recordclass 函數(shù)會創(chuàng)建一個類,該類不參與垃圾回收機制。一般來說,namedtuple 和 recordclass 都可以生成表示記錄或簡單數(shù)據(jù)結(jié)構(gòu)(即非遞歸結(jié)構(gòu))的類。在 Python 中正確使用這二者不會造成循環(huán)引用。因此,recordclass 生成的類實例默認(rèn)情況下不包含 PyGC_Head 片段(這個片段是支持循環(huán)垃圾回收機制的必需字段,或者更準(zhǔn)確地說,在創(chuàng)建類的 PyTypeObject 結(jié)構(gòu)中,flags 字段默認(rèn)情況下不會設(shè)置 Py_TPFLAGS_HAVE_GC 標(biāo)志)。
        大量實例占用的內(nèi)存量要小于帶有 __slots__ 的類實例:
        實例數(shù)
        大小
        1 000 000
        48 Mb
        10 000 000
        480 Mb
        100 000 000
        4.8 Gb

        dataobject
        recordclass 庫提出的另一個解決方案的基本想法為:內(nèi)存結(jié)構(gòu)采用與帶 __slots__ 的類實例同樣的結(jié)構(gòu),但不參與循環(huán)垃圾回收機制。這種類可以通過 recordclass.make_dataclass 函數(shù)生成:
        >>>?Point?=?make_dataclass('Point',?('x',?'y',?'z'))
        這種方式創(chuàng)建的類默認(rèn)會生成可修改的實例。
        另一種方法是從 recordclass.dataobject 繼承:
        class?Point(dataobject):
        ????x:int
        ????y:int
        ????z:int
        這種方法創(chuàng)建的類實例不會參與循環(huán)垃圾回收機制。內(nèi)存中實例的結(jié)構(gòu)與帶有 __slots__ 的類相同,但沒有 PyGC_Head:
        字段
        大?。ㄗ止?jié))
        PyObject_HEAD
        16
        ob_size
        8
        x
        8
        y
        8
        z
        8
        總計:
        48
        >>>?ob?=?Point(1,2,3)
        >>>?print(sys.getsizeof(ob))
        40
        如果想訪問字段,則需要使用特殊的描述符來表示從對象開頭算起的偏移量,其位置位于類字典內(nèi):
        mappingproxy({'__new__':?0x7f203c4e6be0>,
        ??????????????.......................................
        ??????????????'x':?0x7f203c55c690>,
        ??????????????'y':?0x7f203c55c670>,
        ??????????????'z':?0x7f203c55c410>})
        大量實例占用的內(nèi)存量在 CPython 實現(xiàn)中是最小的:
        實例數(shù)
        大小
        1 000 000
        40 Mb
        10 000 000
        400 Mb
        100 000 000
        4.0 Gb

        Cython
        還有一個基于 Cython(https://cython.org/)的方案。該方案的優(yōu)點是字段可以使用 C 語言的原子類型。訪問字段的描述符可以通過純 Python 創(chuàng)建。例如:
        cdef?class?Python:
        ????cdef?public?int?x,?y,?z

        ?def?__init__(self,?x,?y,?z):
        ??????self.x?=?x
        ??????self.y?=?y
        ??????self.z?=?z
        本例中實例占用的內(nèi)存更?。?/span>
        >>>?ob?=?Point(1,2,3)
        >>>?print(sys.getsizeof(ob))
        32
        內(nèi)存結(jié)構(gòu)如下:
        字段
        大?。ㄗ止?jié))
        PyObject_HEAD
        16
        x
        4
        y
        4
        z
        4
        nycto
        4
        總計:
        32
        大量副本所占用的內(nèi)存量也很小:
        實例數(shù)
        大小
        1 000 000
        32 Mb
        10 000 000
        320 Mb
        100 000 000
        3.2 Gb
        但是,需要記住在從 Python 代碼訪問時,每次訪問都會引發(fā) int 類型和 Python 對象之間的轉(zhuǎn)換。

        Numpy
        使用擁有大量數(shù)據(jù)的多維數(shù)組或記錄數(shù)組會占用大量內(nèi)存。但是,為了有效地利用純 Python 處理數(shù)據(jù),你應(yīng)該使用 Numpy 包提供的函數(shù)。
        >>>?Point?=?numpy.dtype(('x',?numpy.int32),?('y',?numpy.int32),?('z',?numpy.int32)])
        一個擁有 N 個元素、初始化成零的數(shù)組可以通過下面的函數(shù)創(chuàng)建:
        ?>>>?points?=?numpy.zeros(N,?dtype=Point)
        內(nèi)存占用是最小的:
        實例數(shù)
        大小
        1 000 000
        12 Mb
        10 000 000
        120 Mb
        100 000 000
        1.2 Gb
        一般情況下,訪問數(shù)組元素和行會引發(fā) Python 對象與 C 語言 int 值之間的轉(zhuǎn)換。如果從生成的數(shù)組中獲取一行結(jié)果,其中包含一個元素,其內(nèi)存就沒那么緊湊了:
        ??>>>?sys.getsizeof(points[0])
        ??68
        因此,如上所述,在 Python 代碼中需要使用 numpy 包提供的函數(shù)來處理數(shù)組。
        總結(jié)
        在本文中,我們通過一個簡單明了的例子,求證了 Python 語言(CPython)社區(qū)的開發(fā)人員和用戶可以真正減少對象占用的內(nèi)存量。
        原文:https://habr.com/en/post/458518


        由于微信平臺算法改版,公號內(nèi)容將不再以時間排序展示,如果大家想第一時間看到我們的推送,強烈建議星標(biāo)我們和給我們多點點【在看】。星標(biāo)具體步驟為:


        (1)點擊頁面最上方“小詹學(xué)Python”,進(jìn)入公眾號主頁。


        (2)點擊右上角的小點點,在彈出頁面點擊“設(shè)為星標(biāo)”,就可以啦。


        感謝支持,比心。

        瀏覽 33
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            暴躁欧美熟妇A片天天 | 人妻精品在线 | 操B理伦在现现看 | 一级毛片免费 | 国产视频久久 | 亚洲性爱中文字幕 | 一级特黄大片在线观看 | 性XXX欧美老妇506070 | 深爱激情网开心五月天 | 国产一级a毛一级a看免费人娇 |