1. 程式人生 > >Python 記憶體分配時的小祕密

Python 記憶體分配時的小祕密

Python 中的sys 模組極為基礎而重要,它主要提供了一些給直譯器使用(或由它維護)的變數,以及一些與直譯器強互動的函式。

本文將會頻繁地使用該模組的getsizeof() 方法,因此,我先簡要介紹一下:

  • 該方法用於獲取一個物件的位元組大小(bytes)
  • 它只計算直接佔用的記憶體,而不計算物件內所引用物件的記憶體

這裡有個直觀的例子:

import sys

a = [1, 2]
b = [a, a]  # 即 [[1, 2], [1, 2]]

# a、b 都只有兩個元素,所以直接佔用的大小相等
sys.getsizeof(a) # 結果:80
sys.getsizeof(b) # 結果:80

上例說明了一件事:一個靜態建立的列表,如果只包含兩個元素,那它自身佔用的記憶體就是 80 位元組,不管其元素所指向的物件是什麼。

好了,擁有這把測量工具,我們就來探究一下 Python 的內建物件都藏了哪些小祕密吧。

1、空物件不是“空”的!

對於我們熟知的一些空物件,例如空字串、空列表、空字典等等,不知道大家是否曾好奇過,是否曾思考過這些問題:空的物件是不是不佔用記憶體呢?如果佔記憶體,那佔用多少呢?為什麼是這樣分配的呢?

直接上程式碼吧,一起來看看幾類基本資料結構的空物件的大小:

import sys
sys.getsizeof("")      # 49
sys.getsizeof([])      # 64
sys.getsizeof(())      # 48
sys.getsizeof(set())   # 224
sys.getsizeof(dict())  # 240

# 作為參照:
sys.getsizeof(1)       # 28
sys.getsizeof(True)    # 28

可見,雖然都是空物件,但是這些物件在記憶體分配上並不為“空”,而且分配得還挺大(記住這幾個數字哦,後面會考)。

排一下序:基礎數字<空元組 < 空字串 < 空列表 < 空集合 < 空字典。

這個小祕密該怎麼解釋呢?

因為這些空物件都是容器,我們可以抽象地理解:它們的一部分記憶體用於建立容器的骨架、記錄容器的資訊(如引用計數、使用量資訊等等)、還有一部分記憶體則是預分配的。

2、記憶體擴充不是均勻的!

空物件並不為空,一部分原因是 Python 直譯器為它們預分配了一些初始空間。在不超出初始記憶體的情況下,每次新增元素,就使用已有記憶體,因而避免了再去申請新的記憶體。

那麼,如果初始記憶體被分配完之後,新的記憶體是怎麼分配的呢?

import sys
letters = "abcdefghijklmnopqrstuvwxyz"

a = []
for i in letters:
    a.append(i)
    print(f'{len(a)}, sys.getsizeof(a) = {sys.getsizeof(a)}')
    
b = set()
for j in letters:
    b.add(j)
    print(f'{len(b)}, sys.getsizeof(b) = {sys.getsizeof(b)}')

c = dict()
for k in letters:
    c[k] = k
    print(f'{len(c)}, sys.getsizeof(c) = {sys.getsizeof(c)}')

分別給三類可變物件新增 26 個元素,看看結果如何:

由此能看出可變物件在擴充時的祕密:

  • 超額分配機制: 申請新記憶體時並不是按需分配的,而是多分配一些,因此當再新增少量元素時,不需要馬上去申請新記憶體
  • 非均勻分配機制: 三類物件申請新記憶體的頻率是不同的,而同一類物件每次超額分配的記憶體並不是均勻的,而是逐漸擴大的

3、列表不等於列表!

以上的可變物件在擴充時,有相似的分配機制,在動態擴容時可明顯看出效果。

那麼,靜態建立的物件是否也有這樣的分配機制呢?它跟動態擴容比,是否有所區別呢?

先看看集合與字典:

# 靜態建立物件
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6}

sys.getsizeof(set_1)  # 224
sys.getsizeof(set_2)  # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368

看到這個結果,再對比上一節的截圖,可以看出:在元素個數相等時,靜態建立的集合/字典所佔的記憶體跟動態擴容時完全一樣。

這個結論是否適用於列表物件呢?一起看看:

list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e']

sys.getsizeof(list_1)  # 80
sys.getsizeof(list_2)  # 88
sys.getsizeof(list_3)  # 96
sys.getsizeof(list_4)  # 104

上一節的截圖顯示,列表在前 4 個元素時都佔 96 位元組,在 5 個元素時佔 128 位元組,與這裡明顯矛盾。

所以,這個祕密昭然若揭:在元素個數相等時,靜態建立的列表所佔的記憶體有可能小於動態擴容時的記憶體!

也就是說,這兩種列表看似相同,實際卻不同!列表不等於列表!

4、消減元素並不會釋放記憶體!

前面提到了,擴充可變物件時,可能會申請新的記憶體。

那麼,如果反過來縮減可變物件,減掉一些元素後,新申請的記憶體是否會自動回收掉呢?

import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5)      # 擴充後:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 擴充後:128
a.pop()          # 縮減後:[1, 2, 3, 4]
sys.getsizeof(a) # 縮減後:128

如程式碼所示,列表在一擴一縮後,雖然回到了原樣,但是所佔用的記憶體空間可沒有自動釋放啊。其它的可變物件同理。

這就是 Python 的小祕密了,“胖子無法減重原理” :瘦子變胖容易,縮減身型也容易,但是體重減不掉,哈哈~~~

5、空字典不等於空字典!

使用 pop() 方法,只會縮減可變物件中的元素,但並不會釋放已申請的記憶體空間。

還有個 clear() 方法,它會清空可變物件的所有元素,讓我們試試看吧:

import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3}

sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240

a.clear()        # 清空後:[]
b.clear()        # 清空後:set()
c.clear()        # 清空後:{},也即 dict()

呼叫 clear() 方法,我們就獲得了幾個空物件。

在第一小節裡,它們的記憶體大小已經被查驗過了。(前面說過會考的,請默寫 回看下)

但是,如果這時再去查驗的話,你會驚訝地發現,這些空物件的大小跟前面查的並不完全一樣!

# 承接前面的清空操作:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72

空列表與空元組的大小不變,然而空字典(72)竟然比前面的空字典(240)要小很多!

也就是說,列表與元組在清空元素後,回到起點不變初心,然而,字典這傢伙卻是“賠了夫人又折兵”,不僅把“吃”進去的全吐出來了,還把自己的老本給虧掉了!

字典的這個祕密藏得挺深的,說實話我也是剛剛獲知,百思不得其解……

以上就是 Python 在分配記憶體時的幾個小祕密啦,看完之後,你是否覺得漲見識了呢?

你想明白了幾個呢,又產生了多少新的謎團呢?歡迎留言一起交流哦~

對於那些沒有充分解釋的小祕密,今後我們再慢慢揭祕……

作者簡介: 豌豆花下貓,生於廣東畢業於武大,現為蘇漂程式設計師,有一些極客思維,也有一些人文情懷,有一些溫度,還有一些態度。公眾號:「Python貓」(python_cat)

相關推薦

Python 記憶體分配祕密

Python 中的sys 模組極為基礎而重要,它主要提供了一些給直譯器使用(或由它維護)的變數,以及一些與直譯器強互動的函式。 本文將會頻繁地使用該模組的getsizeof() 方法,因此,我先簡要介紹一下: 該方法用於獲取一個物件的位元組大小(bytes) 它只計算直接佔用的記憶體,而不計算物件內所引

python記憶體分配機制

python中數值型別是不可變物件,當程式試圖改變資料的值時,程式會重新生成新的資料,而不是改變原來的資料。 python函式的引數都是物件的引用,如果在引用不可變物件時嘗試修改物件,程式會在函式中生

關於記憶體分配malloc()和calloc()的區別

動態分配記憶體空間,較為熟悉的是malloc(),但有時也會用calloc()。兩者有何區別呢? 先寫一下兩者的常規用法示例吧。 void *malloc(size_t size); void *calloc(size_t count,size_t size); 可見

[譯]Python 記憶體分配 垃圾回收

原文 譯文 Python主要使用兩個策略實現記憶體分配。 引用計數 垃圾回收 引用計數 統計在系統中,其他物件引用某個物件的次數。當一個引用移除了,這個物件的引用計數減1。引用計數變為0時物件就被回收。 但是引用計數無法解決引用環的問

Java heap space造成tomcat響應時間過長,原因在JVM記憶體分配,解決方法

使用Java程式從資料庫中查詢大量的資料時出現異常:java.lang.OutOfMemoryError: Java heap space 在JVM中如果98%的時間是用於GC且可用的 Heap size 不足2%的時候將丟擲此異常資訊。 JVM堆的設定是指java程式

Python的幾個程序,其實我覺得可以稱作初學的基礎算法

基本 什麽 否則 col 重新 保留 put span pri 昨天學習的,今天做一下整理,以前學過幾天c,感覺什麽都沒有搞出來,有點泄氣,看到Python後試試,從最基本的東西學起,希望不要辜負我的這一點熱情。 if語句的應用 1 n=1 2 while

GC發生記憶體分配和回收策略

在《深入理解java虛擬機器》一書中讀到3.6章節,記憶體分配和回收策略: 預備知識 java堆=年輕代(Eden+Survivor+Survivor)+老年代 Eden:Survivor:Survivor預設比例8:1:1,每次年輕代使用率90%(Ede

物件賦值為null 記憶體分配情況,以及什麼時候使用效率高

對於成員變數也就是instance member來說是沒區別的,物件初始化的時候會自動賦值成null。但是對於區域性變數也就是local variable來說,不賦值初始化使用編譯會報錯。 對於一般的物件成員來說 分配好空間都會預先分配一個null值。所以寫不寫這個沒什麼特

c語言中較常見的由記憶體分配引起的錯誤_記憶體越界_記憶體未初始化_記憶體_結構體隱含指標

1.指標沒有指向一塊合法的記憶體   定義了指標變數,但是沒有為指標分配記憶體,即指標沒有指向一塊合法的內淺顯的例子就不舉了,這裡舉幾個比較隱蔽的例子。 1.1結構體成員指標未初始化 1 2 3 4 5 6 7

Python程式設計學習5:python id()函式和記憶體分配理解

1.  id()函式可返回物件的記憶體地址python中會為每個物件分配記憶體,哪怕他們的值完全相等。id(object)函式是返回物件object在其生命週期內位於記憶體中的地址,id函式的引數型別是一個物件。如下例子:c, d 和 2.0 地址不同,但值相等。c = 2.

面試知識點-- 作業系統執行可執行程式記憶體分配是怎樣的?

一般認為在c中分為這幾個儲存區:     1. 棧 --有編譯器自動分配釋放      2. 堆 -- 一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收      3. 全域性區(靜態區) -- 全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全

python原始碼分析----記憶體分配(2)

早就應該寫部分的內容了。。。。最近比較負能量。。。傷不起啊。。 上一篇說到了,在python的記憶體分配中兩個非常重要的方法:PyObject_Malloc和PyObject_Free 在具體的來這兩個方法之前,先要看看別的一些東西 //這裡用usedpool構成了一個雙

Java記憶體分配策略,Java執行記憶體分配

Java 記憶體分配策略 Java 程式執行時的記憶體分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種儲存策略使用的記憶體空間主要分別是靜態儲存區(也稱方法區)、棧區和堆區。 靜態儲存區(方法區):主要存放靜態資料、全域性 static 資

java程式執行記憶體分配詳解

一、 基本概念    每執行一個java程式會產生一個java程序,每個java程序可能包含一個或者多個執行緒,每一個Java程序對應唯一一個JVM例項,每一個JVM例項唯一對應一個堆,每一個執行緒有一個自己私有的棧。程序所建立的所有類的例項(也就是物件)或陣列(指的是

JVM記憶體分配_---JVM在進行記憶體回收,是如何識別哪些物件應該放在新生代,哪些物件應該放在老年代的?

首先,瞭解這一過程,必須對堆的記憶體模型進行了解。先看下圖: JVM將堆記憶體分為新生代(1/3的堆記憶體)和老年代(2/3的堆記憶體)兩個區域。 新生代區域一般採用複製演算法對記憶體進行回收。 老年代區域則採用標記清除演算法和標記壓縮演算法對記憶體進

【轉載】關於Python混合程式設計記憶體洩露

       登陸論壇  | 論壇註冊| 加入收藏 | 設為首頁| RSS      首頁Linux頻道軟體下載開發語言嵌入式頻道開源論壇 | php | JSP | ASP | asp.net | JAVA | c/c++/c# | perl | JavaScrip

使用memory_profiler監測python程式碼執行記憶體消耗

前幾天一直在尋找能夠輸出python函式執行時最大記憶體消耗的方式,看了一堆的部落格和知乎,也嘗試了很多方法,最後選擇使用memory_profiler中的mprof功能來進行測量的,它的原理是在程式碼執行過程中每0.1S統計一次記憶體,並生成統計圖。 具體的

Python原始碼學習十一 一個常用的記憶體分配函式

void * _PyObject_DebugMallocApi(char id, size_t nbytes) { uchar *p; /* base address of malloc'ed block */ uchar *tail;

17個新手常見Python運行錯誤

字符 ++ lambda you ssi ref ror scan 做的 當初學 Python 時,想要弄懂 Python 的錯誤信息的含義可能有點復雜。這裏列出了常見的的一些讓你程序 crash 的運行時錯誤。 1)忘記在 if , elif , else , for ,

新手常見Python運行錯誤

before oschina arguments support 復雜 cas ssi egg 必須 1)忘記在 if , elif , else , for , while , class ,def 聲明末尾添加 :(導致 “SyntaxError :invalid sy