獨家|測量、建議、快速上手!你所使用的Python對象佔用了多少內存?(附代碼)

2021-02-21 數據派THU


作者:Gigi Sayfan

翻譯:吳振東

校對:吳金笛

本文約3300字,建議閱讀10分鐘。

本文將介紹多種Python對象分別所佔用的內存,並解釋所選擇的測量方法和函數,為節省內存提供建議。

Python是一種很棒的程式語言。不過它的運行速度很慢,這是由於它具有極大的靈活性和動態特徵所造成的。對於許多應用和領域來說,考慮到它們的要求和各種優化技術,這並不能算是一個問題。眾所周知,Python對象圖(列表、元組和基元類型的嵌套字典)佔用了大量內存。這可能是一個更為嚴格的限制因素,因為這對緩存、虛擬內存、與其他程序的多租戶產生了影響,而且通常會更快地耗盡一種稀缺且昂貴的資源——可用內存。

事實證明,想要弄清楚實際消耗了多少內存並非易事。在本文中,我將向你介紹Python對象內存管理的複雜性,並展示如何準確地去測量所消耗的內存。

在本文中,我只關注CPython——Python程式語言的主要實現。這裡的實驗結論並不適用於其他Python的實現,例如IronPython,Jython和PyPy。

另外,我是在Python 2.7上運行所得到的這些數據。如果是在Python 3中,這些結果可能會略有不同(特別是對於Unicode的字符串),但是理念是基本相同的。


關於Python內存使用的實踐探索

首先,讓我們初步探索一下,來了解Python對象的實際內存使用的具體情況。


內嵌函數sys.getsizeof()

標準庫的sys模塊提供了getsizeof()函數。該函數接收一個對象(和可選的默認值),調用sizeof()方法並返回結果,從而可以讓你所使用的對象具備可檢查性。

getsizeof()

https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&token=1853049065&lang=zh_CN#sys.getsizeof

測量Python對象的內存

首先從數值類型開始:

```python import sys

sys.getsizeof(5) 24 ```

有意思,一個整數(integer)佔用了24位元組。

python sys.getsizeof(5.3) 24

嗯……一個浮點數(float)同樣佔用24位元組。

python from decimal import Decimal sys.getsizeof(Decimal(5.3)) 80

哇哦,80位元組!如此一來你可能要想一想是該用float還是Decimals來表示大量的實數了。

讓我們看一下字符串(strings)和collections:

```python sys.getsizeof(『』) 37 sys.getsizeof(『1』) 38 sys.getsizeof(『1234』) 41

sys.getsizeof(u』』) 50 sys.getsizeof(u』1』) 52 sys.getsizeof(u』1234』) 58 ```

好吧。一個空字符串佔用37位元組,每增加一個字符就增加1個字節。這提出了一個關於對保留多個短字符串的權衡問題,你是願意為每個短字符串支付37位元組的開銷,還是願意為一個長字符串一次性地支付開銷。

Unicode字符串的行為類似,但它的開銷是50位元組,每增加一個字符就會增加2位元組的開銷。如果你使用返回Unicode字符串的庫,而你的文本原本可以用簡單的字符串來表示的話,那麼你就需要考慮下這一點。

順便說一下,在Python 3中,字符串都是Unicode,開銷是49位元組(它們在某處節省了1位元組)。Bytes對象的開銷是33位元組。如果你的程序在內存中需要處理大量的短字符串,而你又很關心程序的性能的話,那麼建議你考慮使用Python 3。

python sys.getsizeof([]) 72 sys.getsizeof([1]) 88 sys.getsizeof([1, 2, 3, 4]) 104 sys.getsizeof(['a long longlong string'])

這是怎麼回事?一個空的list佔用72位元組,但每增加一個int只加大了8位元組,其中一個int佔用24位元組。一個包含長字符串的list只佔用80位元組。

答案其實很簡單。list並不包含int對象本身。它只包含一個佔8位元組(在CPython 64位版本中)指向實際int對象的指針。這意味著getsizeof()函數不返回list的實際內存及其包含的所有對象,而只返回list的內存和指向其對象的指針。 

在下一節中,我將介紹可以解決此問題的deep_getsizeof()函數。

python sys.getsizeof(()) 56 sys.getsizeof((1,)) 64 sys.getsizeof((1, 2, 3, 4)) 88 sys.getsizeof(('a long longlong string',)) 64

對於元組(tuples)來說情況類似。空元組的開銷是56位元組,空list是72位元組。如果你的數據結構包括許多小的不可變的序列,那麼每個序列之間所差的這16位元組是一個非常容易實現的目標。

```python sys.getsizeof(set()) 232 sys.getsizeof(set([1)) 232 sys.getsizeof(set([1, 2, 3, 4])) 232

sys.getsizeof({}) 280 sys.getsizeof(dict(a=1)) 280 sys.getsizeof(dict(a=1, b=2, c=3)) 280 ```

當你添加一個項時,集合(Set)和字典(dictionary)在表面上根本不會有所增長,但請注意它們所帶來的巨大開銷。

原因是Python對象具有巨大的固定開銷。如果你的數據結構由大量的集合對象組成,比如說字符串、列表和字典,每個集合都包含少量的項,你同樣要為之付出沉重的代價。

deep_getsizeof()函數

現在你可能被我上面所提到的嚇出一身冷汗,這同時也證明了sys.getsizeof()只能告訴你原始對象需要多少內存,那麼讓我們來看一種更合適的解決方案。

deep_getsizeof()是向下層遞歸的函數,並且可以計算Python對象圖的的內存實際使用量。

```python from collections import Mapping, Container from sys import getsizeof

def deep_getsizeof(o, ids): 「"」Find the memory footprint of a Python object

這是一個遞歸函數,它向下讀取一個Python對象圖,比如說一個包含列表套用列表的嵌套字典的字典和元組以及集合。

sys.getsizeof函數僅執行較淺的深度。不管它的容器內的每個對象的實際大小,它都將其設為指針。

:param o: the object

:param ids:

:return:

"""

d = deep_getsizeof

if id(o) in ids:

    return 0

r = getsizeof(o)

ids.add(id(o))

if isinstance(o, str) or isinstance(0, unicode):

    return r

if isinstance(o, Mapping):

    return r + sum(d(k, ids) + d(v, ids) for k, v in o.iteritems())

if isinstance(o, Container):

    return r + sum(d(x, ids) for x in o)

return r  ```

對於這個函數來說有幾個有趣的方面。它會考慮多次引用的對象,並通過追蹤對象ID來對它們進行一次計數。這一實現的另一個有趣的特性是它充分利用了collections模塊的抽象基類。這使得這個函數可以非常簡潔地處理任何實現Mapping和Container基類的集合,而不是直接去處理無數集合類型,例如:字符串、Unicode、字節、列表、元組、字典、frozendict, OrderedDict, 集合、 frozenset等等。

讓我們看下它是如何執行的:

python x = '1234567' deep_getsizeof(x, set()) 44

一個長度為7的字符串佔用了44位元組(原開銷37位元組+7個字符佔用7位元組)。

python deep_getsizeof([], set()) 72

空列表佔用72位元組(只有原開銷)。

python deep_getsizeof([x], set()) 124

一個包含字符串x的列表佔用124位元組(72+8+44)。

python deep_getsizeof([x, x, x, x, x], set()) 156

一個包含5個x字符串的列表佔用156位元組(72+5*8+44)。

最後一個例子顯示了deep_getsizeof()只計算一次同一對象(x字符串)的引用,但會把每一個引用的指針計算在內。

處理方式or騙招

事實證明,CPython中有一些騙招,所以你從deep_getsizeof()中所得到的數字並不能完全代表Python程序中的內存使用。

引用計數

Python使用引用計數語義來管理內存。一旦對象不再被使用,就會釋放其內存。但只要存在引用,該對象就不會被釋放。那些循環引用之類的東西會讓你感到很難受。

小對象

CPython可以管理8位元組邊界上的特殊池裡的小對象(小於256位元組)。有1-8位元組的池,9-16位元組的池,一直到249-256位元組的池。當一個10位元組大小的對象被分配時,它會從16位元組池中分配出大小為9-16位元組的對象。因此,即便他只包含10位元組的數據,但它還是會花費16位元組的內存。如果1,000,000個10位元組大小的對象被分配時,實際使用的內存是16,000,000位元組,而不是10,000,000個字節。這其中多出的60%的開銷顯然是微不足道的。

整數

CPython保留了【-5,256】範圍內所有整數的全局列表。這種優化策略是很有意義的,因為小整數隨時隨地都可能會出現。假設每個整數佔用24個字節,那麼這就會為典型的程序節省大量內存。

這意味著CPython為所有這些整數都預先分配了266*24=6384個字節,即便它們中的大部分你用不到。你可以使用id()函數來驗證它,這個函數提供指向實際函數的指針。如果對【-5,256】範圍內的任意x多次調用id(x),那麼每次都會得到相同的結果(對於相同的整數)。但如果你拿超出這個範圍的整數做嘗試,那麼每次得到的結果都不相同(每次都會動態創造一個新的對象)。

這有幾個在這個範圍內的例子:

```python id(-3) 140251817361752

id(-3) 140251817361752

id(-3) 140251817361752

id(201) 140251817366736

id(201) 140251817366736

id(201) 140251817366736 ```

這有幾個超過這個範圍的例子:

```python id(301) 140251846945800

id(301) 140251846945776

id(-6) 140251846946960

id(-6) 140251846946936 ```

Python內存vs系統內存

CPython具有一種所屬性。在很多情況下,當程序中的內存對象不再被引用時,他們不會再返回系統中(例如小對象)。如果你分配和釋放許多對象(屬於同一個8位元組池的),這會對你的程序很有好處,因為不需要去打擾系統,否則代價會是非常昂貴的。不過如果你的程序通常在使用X字節並在偶然情況下使用它100次(例如僅在啟動時解析和處理大配置文件),那麼效果就不是特別好了。

現在,100X的內存有可能被毫無用處的困在你的程序裡,永遠不會被再次利用,而且也拒絕被系統分配給其他程序。更具諷刺意義的是,如果你使用處理模塊來運行程序的多個實例,那麼就會嚴重限制你在給定計算機上可以運行的實例數。

內存剖析

想要衡量和測量程序的實際內存使用情況,可以使用memory_profiler模塊。我嘗試了一下,不確定所得出的結果是否可信。它使用起來非常簡單。你裝飾一個函數(可能是@profiler裝飾器的主函數0函數),當程序退出時,內存分析器會列印出一份標準輸出的簡潔報告,顯示每行的總內存和內存變化。我是在分析器下運行的這個示例。

memory_profiler

https://pypi.python.org/pypi/memory_profiler

```python from memory_profiler import profile

@profile def main(): a = [] b = [] c = [] for i in range(100000): a.append(5) for i in range(100000): b.append(300) for i in range(100000): c.append(『123456789012345678901234567890』) del a del b del c

print 'Done!' if __name__ == '__main__':

main() ```

Here is the output:

Line #    Mem usage    Increment   Line Contents

================================================

     3     22.9 MiB      0.0 MiB   @profile

     4                             def main():

     5     22.9 MiB      0.0 MiB       a = []

     6     22.9 MiB      0.0 MiB       b = []

     7     22.9 MiB      0.0 MiB       c = []

     8     27.1 MiB      4.2 MiB       for i in range(100000):

     9     27.1 MiB      0.0 MiB           a.append(5)

    10     27.5 MiB      0.4 MiB       for i in range(100000):

    11     27.5 MiB      0.0 MiB           b.append(300)

    12     28.3 MiB      0.8 MiB       for i in range(100000):

    13     28.3 MiB      0.0 MiB           c.append('123456789012345678901234567890')

    14     27.7 MiB     -0.6 MiB       del a

    15     27.9 MiB      0.2 MiB       del b

    16     27.3 MiB     -0.6 MiB       del c

    17

    18     27.3 MiB      0.0 MiB       print 'Done!'

如你所見,這裡的內存開銷是22.9MB。在【-5,256】範圍內外添加整數和添加字符串時內存不增加的原因是在所有情況下都使用單個對象。目前尚不清楚為什麼第8行的第一個range(1000)循環增加了4.2MB,而第10行的第二個循環只增加了0.4MB,第12行的第三個循環增加了0.8MB。最後,當刪除a,b和C列表時,為a和c釋放了0.6MB,但是為b添加了0.2MB。對於這些結果我並不是特別理解。


總結

CPython為它的對象使用了大量內存,也使用了各種技巧和優化方式來進行內存管理。通過跟蹤對象的內存使用情況並了解內存管理模型,可以顯著減少程序的內存佔用。

學習Python,無論你是剛入門的新手還是經驗豐富的編碼人員,都可以使用我們的完整Python教程指南來學習。

原文標題:

Understand How Much Memory Your Python Objects Use

原文連結:

https://code.tutsplus.com/tutorials/understand-how-much-memory-your-python-objects-use--cms-25609

編輯:王菁

校對:林亦霖

吳振東,法國洛林大學計算機與決策專業碩士。現從事人工智慧和大數據相關工作,以成為數據科學家為終生奮鬥目標。來自山東濟南,不會開挖掘機,但寫得了Java、Python和PPT。

工作內容:需要一顆細緻的心,將選取好的外文文章翻譯成流暢的中文。如果你是數據科學/統計學/計算機類的留學生,或在海外從事相關工作,或對自己外語水平有信心的朋友歡迎加入翻譯小組。

你能得到:定期的翻譯培訓提高志願者的翻譯水平,提高對於數據科學前沿的認知,海外的朋友可以和國內技術應用發展保持聯繫,THU數據派產學研的背景為志願者帶來好的發展機遇。

其他福利:來自於名企的數據科學工作者,北大清華以及海外等名校學生他們都將成為你在翻譯小組的夥伴。

點擊文末「閱讀原文」加入數據派團隊~


轉載須知

如需轉載,請在開篇顯著位置註明作者和出處(轉自:數據派ID:datapi),並在文章結尾放置數據派醒目二維碼。有原創標識文章,請發送【文章名稱-待授權公眾號名稱及ID】至聯繫郵箱,申請白名單授權並按要求編輯。

發布後請將連結反饋至聯繫郵箱(見下方)。未經許可的轉載以及改編者,我們將依法追究其法律責任。

點擊「閱讀原文」擁抱組織

相關焦點

  • ​如何使用生成器減少內存佔用,並讓Python代碼運行更快?
    本文轉載自公眾號「讀芯術」(ID:AI_Discovery)如何使用生成器減少內存佔用並讓Python代碼運行更快,關乎你「代碼人生」的生死存亡。 然而,當我剛開始學習Python生成器時,並不知道它最後會顯得如此重要。 但在學習機器學習的過程中需要編寫自定義函數時,它發揮了不可取代的作用。
  • 技能分享:如何用生成器減少內存佔用,讓Python代碼運行更快?
    圖源:Unsplash如何使用生成器減少內存佔用並讓Python代碼運行更快,關乎你「代碼人生」的生死存亡。生成器函數允許聲明一個類似於迭代器的函數,使得程式設計師可以以快速,簡便和簡潔的方式創建一個迭代器。迭代器是一個可以進行迭代的對象,用於對一個數據容器進行抽象,使其表現得像可迭代的對象。常見的可迭代對象的一些示例有字符串、列表和字典。
  • 系統內存/進程內存知識掃盲
    虛擬內存空間大只能表示程序運行過程中可訪問的空間比較大,不代表物理內存空間佔用也大。注意:這裡的虛擬內存和最上面介紹的使用磁碟作為swap的虛擬內存是不一樣的概念,注意區別!!CODE  可執行代碼佔用的物理內存大小,單位kb   DATA  可執行代碼以外的部分(數據段+棧)佔用的物理內存大小,單位kb  RES就是進程實實在在佔用的物理內存。
  • 使用模板匹配在Python上進行對象檢測!(附代碼)
    了解如何在沒有機器學習或任何框架的情況下在Python上進行對象檢測每當我們聽說「 對象檢測 」時,我們就會想到機器學習以及不同的框架。但是我們實際上可以在不使用機器學習或任何其他框架的情況下進行對象檢測。在本文中,我將向您展示如何僅使用Python進行操作。
  • 獨家 | 手把手教你使用OpenCV庫(附實例、Python代碼解析)
    本文將通過幾個簡單的實例帶你上手OpenCV庫,新手必備! OpenCV Python 教程 在這個OpenCV Python 的教程中, 我們將使用Python中的OpenCV庫來介紹計算機視覺的各個方面。OpenCV 長期以來一直是軟體開發的重要部分。
  • Python中的內存分配和管理
    了解內存管理可以幫助您編寫高效的Python代碼。儘管您可能無法控制內存分配,但是您可以優化程序來更好地分配內存。在深入研究之前,請記住:在python中,一切都是對象。與C,C ++或Java不同,值存儲在內存中,並且變量指向該內存位置。
  • 你必須掌握的20個python代碼,短小精悍,用處無窮
    而python編程中的一些小的技巧,運用的恰當,會讓你的程序事半功倍。以下的20個小的程序段,看似非常的簡單,但是卻非常的有技巧性,並且對個人的編程能力是一個很好的檢驗,大家應該在日常的編程中多多使用,多多練習。1.字符串的翻轉
  • Python 性能分析入門指南
    分析程序的性能可以歸結為回答四個基本問題:正運行的多快速度瓶頸在哪裡內存使用率是多少內存洩露在哪裡下面,我們將用一些神奇的工具深入到這些問題的答案中去。用 time 粗粒度的計算時間讓我們開始通過使用一個快速和粗暴的方法計算我們的代碼:傳統的 unix time 工具。
  • python代碼結構:使用if語句、while循環和for迭代,附詳細說明!
    1.使用#注釋在Python中使用#字符標記注釋,從#開始到當前行結束的部分都是注釋。注釋可以單獨一行,也可與代碼同行,同行放在代碼後面。特殊說明:(1)Python沒有多行注釋的符號。(2)如果#出現在文本串中,將回歸普通字符#的角色2.使用\連接程序在合理的長度下是易讀的。一行程序的(非強制性)最大長度建議為80個字符。如果在該長度下寫不完代碼,可以使用連接符\(反斜線)。
  • 獲得PHP代碼佔用內存的情況
    下面是使用示例:3echo memory_get_usage(), '<br />';        4$tmp = str_repeat('http://www.xxx.net/', 4000);5echo memory_get_usage(), '<br />'; 7echo memory_get_usage();        上面的程序後面的注釋代表了它們的輸出
  • 代碼跑得慢甩鍋Python?手把手教你如何給代碼提速30%
    其實某個特定程序(無論使用何種程式語言)的運行速度是快還是慢,在很大程度上取決於編寫該程序的開發人員自身素質,以及他們編寫優化而高效代碼的能力。Medium上一位小哥就詳細講了講如何讓python提速30%,以此證明代碼跑得慢不是python的問題,而是代碼本身的問題。
  • Python面向對象程式語言
    它使你能夠專注於解決問題而不是去搞明白語言本身。易學————就如同你即將看到的一樣,Python極其容易上手。前面已經提到了,Python有極其簡單的語法。免費、開源————Python是FLOSS(自由/開放源碼軟體)之一。簡單地說,你可以自 由地發布這個軟體的拷貝、閱讀它的原始碼、對它做改動、把它的一部分用於新的自由軟體中。
  • Pandas學習筆記(二三):內存佔用
    通常情況下,Pandas對讀取的數據列默認是設置為object數據類型,這種通用類型因自身的兼容性會導致所讀取的數據佔據較大的內存空間,倘若能給它們設置合適的數據類型,就可以降低該數據集的實際內存佔用,從而提升運行效率。
  • Python從入門到精通,這篇文章為你列出了25個關鍵技術點(附代碼)
    如果你想使用 C 模塊,那麼你可以使用 PyImport_ImportModule。此外,如果你想在兩個不同模塊中使用定義相同的對象,那麼可以將 import 和 from 結合起來導入模塊。 09包 (Packages)Python 中包是模塊的目錄。
  • Python2 已終結,入手Python 3,你需要這30個技巧
    這篇教程有 30 個你會喜歡的方法。勤勞的程式設計師們,這裡有 30 條使用 Python 時實用的建議和小技巧。你可以把讀這篇文章當做工作間隙的小憩,而且我保證你學到的東西會跟工作時一樣多。1.檢查你的對象佔用了多少內存你可以使用 sys.getsizeof() 來查看你創建的對象佔用的內存大小:哇,等一下,為什麼這麼大的 list 只有 48 字節?這是因為 range 函數隻返回了一個類似 list 的類。
  • 教程 | 簡單實用的pandas技巧:如何將內存佔用降低90%
    數據科學博客 Dataquest.io 發布了一篇關於如何優化 pandas 內存佔用的教程:僅需進行簡單的數據類型轉換,就能夠將一個棒球比賽數據集的內存佔用減少了近 90%,機器之心對本教程進行了編譯介紹。當使用 pandas 操作小規模數據(低於 100 MB)時,性能一般不是問題。
  • Android 內存管理詳解
    這意味著應用程式修改的任何內存(無論是通過分配新對象通過映射頁面)都將保留在RAM中,並且不能被分頁。應用程式釋放內存的唯一方法是釋放應用程式持有的對象引用,即使垃圾收集器回收(GC)回收內存 。比如:如果系統想要在其他地方使用該內存,則可以將任何未經修改的映射到mmap中文件(例如代碼)分頁出RAM。本頁面介紹了Android如何管理應用程式進程和內存分配。
  • 什麼是Python的迭代器和生成器?(附代碼)
    utm_source=blog&utm_medium=python-iterators-and-generators這是我們要介紹的內容:什麼是可迭代對象?什麼是Python迭代器?在Python中創建一個迭代器熟悉Python中的生成器實現Python中的生成器表達式為什麼你應該使用迭代器?
  • numpy中的數據類型對象有哪些
    Numpy數據類型numpy是一個python擴展包,它可以為我們提供更精確的科學技術,更強大的數學能力。為此,numpy定義了比python更豐富的數據類型來達成目的。需要理解的是,Numpy中的數據類型,和python本身的數據類型是不同的。Numpy中的數據類型,實質是數據類型對象dtype的實例。Numpy中的Ndarray對象可以幫助我們構建N維數組對象。我們知道,對於數組來說,最大特點是,對於給定長度的數組,其在內存中所佔用的空間大小是預先分配的,並且,每個元素所佔用的空間大小是相當的。
  • 零基礎菜鳥如何快速上手Python
    那麼作為零基礎菜鳥的你,如何在Python入門的時候能避開大多數的雷區,這篇文章值得你花10分鐘的時候仔細閱讀。零基礎菜鳥如何快速上手Python為了幫助大家更輕鬆的學好Python,無私分享一套Python學習資料,希望對正在學習的你有所幫助!