自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Python在計(jì)算內(nèi)存時(shí)應(yīng)該注意的問題?

開發(fā) 后端
我之前的一篇文章,帶大家揭曉了 Python 在給內(nèi)置對象分配內(nèi)存時(shí)的 5 個(gè)奇怪而有趣的小秘密。文中使用了sys.getsizeof()來計(jì)算內(nèi)存,但是用這個(gè)方法計(jì)算時(shí),可能會出現(xiàn)意料不到的問題。

 我之前的一篇文章,帶大家揭曉了 Python 在給內(nèi)置對象分配內(nèi)存時(shí)的 5 個(gè)奇怪而有趣的小秘密。文中使用了sys.getsizeof()來計(jì)算內(nèi)存,但是用這個(gè)方法計(jì)算時(shí),可能會出現(xiàn)意料不到的問題。

[[317078]]

文檔中關(guān)于這個(gè)方法的介紹有兩層意思:

  • 該方法用于獲取一個(gè)對象的字節(jié)大小(bytes)
  • 它只計(jì)算直接占用的內(nèi)存,而不計(jì)算對象內(nèi)所引用對象的內(nèi)存

也就是說,getsizeof() 并不是計(jì)算實(shí)際對象的字節(jié)大小,而是計(jì)算“占位對象”的大小。如果你想計(jì)算所有屬性以及屬性的屬性的大小,getsizeof() 只會停留在第一層,這對于存在引用的對象,計(jì)算時(shí)就不準(zhǔn)確。

例如列表 [1,2],getsizeof() 不會把列表內(nèi)兩個(gè)元素的實(shí)際大小算上,而只是計(jì)算了對它們的引用。舉一個(gè)形象的例子,我們把列表想象成一個(gè)箱子,把它存儲的對象想象成一個(gè)個(gè)球,現(xiàn)在箱子里有兩張紙條,寫上了球 1 和球 2 的地址(球不在箱子里),getsizeof() 只是把整個(gè)箱子稱重(含紙條),而沒有根據(jù)紙條上地址,找到兩個(gè)球一起稱重。

1、計(jì)算的是什么?

我們先來看看列表對象的情況:

 

 

如圖所示,單獨(dú)計(jì)算 a 和 b 列表的結(jié)果是 36 和 48,然后把它們作為 c 列表的子元素時(shí),該列表的計(jì)算結(jié)果卻僅僅才 36。(PS:我用的是 32 位解釋器)

如果不使用引用方式,而是直接把子列表寫進(jìn)去,例如 “d = [[1,2],[1,2,3,4,5]]”,這樣計(jì)算 d 列表的結(jié)果也還是 36,因?yàn)樽恿斜硎仟?dú)立的對象,在 d 列表中存儲的是它們的 id。

也就是說:getsizeof() 方法在計(jì)算列表大小時(shí),其結(jié)果跟元素個(gè)數(shù)相關(guān),但跟元素本身的大小無關(guān)。

下面再看看字典的例子:

 

明顯可以看出,三個(gè)字典實(shí)際占用的全部內(nèi)存不可能相等,但是 getsizeof() 方法給出的結(jié)果卻相同,這意味著它只關(guān)心鍵的數(shù)量,而不關(guān)心實(shí)際的鍵值對是什么內(nèi)容,情況跟列表相似。

2、“淺計(jì)算”與其它問題

有個(gè)概念叫“淺拷貝”,指的是 copy() 方法只拷貝引用對象的內(nèi)存地址,而非實(shí)際的引用對象。類比于這個(gè)概念,我們可以認(rèn)為 getsizeof() 是一種“淺計(jì)算”。

“淺計(jì)算”不關(guān)心真實(shí)的對象,所以其計(jì)算結(jié)果只是一個(gè)假象。這是一個(gè)值得注意的問題,但是注意到這點(diǎn)還不夠,我們還可以發(fā)散地思考如下的問題:

  • “淺計(jì)算”方法的底層實(shí)現(xiàn)是怎樣的?
  • 為什么 getsizeof() 會采用“淺計(jì)算”的方法?

關(guān)于第一個(gè)問題,getsizeof(x) 方法實(shí)際會調(diào)用 x 對象的__sizeof__() 魔術(shù)方法,對于內(nèi)置對象來說,這個(gè)方法是通過 CPython 解釋器實(shí)現(xiàn)的。

我查到這篇文章《Python中對象的內(nèi)存使用(一)》,它分析了 CPython 源碼,最終定位到的核心代碼是這一段:

 

  1. /*longobject.c*/ 
  2.  
  3. static Py_ssize_t 
  4. int___sizeof___impl(PyObject *self) 
  5.     Py_ssize_t res; 
  6.  
  7.     res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit); 
  8.     return res; 

我看不懂這段代碼,但是可以知道的是,它在計(jì)算 Python 對象的大小時(shí),只跟該對象的結(jié)構(gòu)體的屬性相關(guān),而沒有進(jìn)一步作“深度計(jì)算”。

對于 CPython 的這種實(shí)現(xiàn),我們可以注意到兩個(gè)層面上的區(qū)別:

  • 字節(jié)增大:int 類型在 C 語言中只占到 4 個(gè)字節(jié),但是在 Python 中,int 其實(shí)是被封裝成了一個(gè)對象,所以在計(jì)算其大小時(shí),會包含對象結(jié)構(gòu)體的大小。在 32 位解釋器中,getsizeof(1) 的結(jié)果是 14 個(gè)字節(jié),比數(shù)字本身的 4 字節(jié)增大了。
  • 字節(jié)減少:對于相對復(fù)雜的對象,例如列表和字典,這套計(jì)算機(jī)制由于沒有累加內(nèi)部元素的占用量,就會出現(xiàn)比真實(shí)占用內(nèi)存小的結(jié)果。

由此,我有一個(gè)不成熟的猜測:基于“一切皆是對象”的設(shè)計(jì)原則,int 及其它基礎(chǔ)的 C 數(shù)據(jù)類型在 Python 中被套上了一層“殼”,所以需要一個(gè)方法來計(jì)算它們的大小,也即是 getsizeof()。

官方文檔中說“All built-in objects will return correct results” [1],指的應(yīng)該是數(shù)字、字符串和布爾值之類的簡單對象。但是不包括列表、元組和字典等在內(nèi)部存在引用關(guān)系的類型。

為什么不推廣到所有內(nèi)置類型上呢?我未查到這方面的解釋,若有知情的同學(xué),煩請告知。

3、“深計(jì)算”與其它問題

與“淺計(jì)算”相對應(yīng),我們可以定義出一種“深計(jì)算”。對于前面的兩個(gè)例子,“深計(jì)算”應(yīng)該遍歷每個(gè)內(nèi)部元素以及可能的子元素,累加計(jì)算它們的字節(jié),最后算出總的內(nèi)存大小。

那么,我們應(yīng)該注意的問題有:

  • 是否存在“深計(jì)算”的方法/實(shí)現(xiàn)方案?
  • 實(shí)現(xiàn)“深計(jì)算”時(shí)應(yīng)該注意什么?

Stackoverflow 網(wǎng)站上有個(gè)年代久遠(yuǎn)的問題“How do I determine the size of an object in Python?” [2],實(shí)際上問的就是如何實(shí)現(xiàn)“深計(jì)算”的問題。

有不同的開發(fā)者貢獻(xiàn)了兩個(gè)項(xiàng)目:pympler 和 pysize :第一個(gè)項(xiàng)目已發(fā)布在 Pypi 上,可以“pip install pympler”安裝;第二個(gè)項(xiàng)目爛尾了,作者也沒發(fā)布到 Pypi 上(注:Pypi 上已有個(gè) pysize 庫,是用來做格式轉(zhuǎn)化的,不要混淆),但是可以在 Github 上獲取到其源碼。

對于前面的兩個(gè)例子,我們可以拿這兩個(gè)項(xiàng)目分別測試一下:

 

 

 

 

單看數(shù)值的話,pympler 似乎確實(shí)比 getsizeof() 合理多了。

再看看 pysize,直接看測試結(jié)果是(獲取其源碼過程略):

 

  1. 64 
  2. 118 
  3. 190 
  4. 206 
  5. 300281 
  6. 30281 

可以看出,它比 pympler 計(jì)算的結(jié)果略小。就兩個(gè)項(xiàng)目的完整度、使用量與社區(qū)貢獻(xiàn)者規(guī)模來看,pympler 的結(jié)果似乎更為可信。

那么,它們分別是怎么實(shí)現(xiàn)的呢?那微小的差異是怎么導(dǎo)致的?從它們的實(shí)現(xiàn)方案中,我們可以學(xué)習(xí)到什么呢?

pysize 項(xiàng)目很簡單,只有一個(gè)核心方法:

 

  1. def get_size(obj, seen=None): 
  2.     """Recursively finds size of objects in bytes""" 
  3.     size = sys.getsizeof(obj) 
  4.     if seen is None: 
  5.         seen = set() 
  6.     obj_id = id(obj) 
  7.     if obj_id in seen: 
  8.         return 0 
  9.     # Important mark as seen *before* entering recursion to gracefully handle 
  10.     # self-referential objects 
  11.     seen.add(obj_id) 
  12.     if hasattr(obj, '__dict__'): 
  13.         for cls in obj.__class__.__mro__: 
  14.             if '__dict__' in cls.__dict__: 
  15.                 d = cls.__dict__['__dict__'
  16.                 if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d): 
  17.                     size += get_size(obj.__dict__, seen) 
  18.                 break 
  19.     if isinstance(obj, dict): 
  20.         size += sum((get_size(v, seen) for v in obj.values())) 
  21.         size += sum((get_size(k, seen) for k in obj.keys())) 
  22.     elif hasattr(obj, '__iter__'and not isinstance(obj, (str, bytes, bytearray)): 
  23.         size += sum((get_size(i, seen) for i in obj)) 
  24.  
  25.     if hasattr(obj, '__slots__'): # can have __slots__ with __dict__ 
  26.         size += sum(get_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s)) 
  27.  
  28.     return size 

除去判斷__dict__和 __slots__屬性的部分(針對類對象),它主要是對字典類型及可迭代對象(除字符串、bytes、bytearray)作遞歸的計(jì)算,邏輯并不復(fù)雜。

以 [1,2] 這個(gè)列表為例,它先用 sys.getsizeof() 算出 36 字節(jié),再計(jì)算內(nèi)部的兩個(gè)元素得 14*2=28 字節(jié),最后相加得到 64 字節(jié)。

相比之下,pympler 所考慮的內(nèi)容要多很多,入口在這:

 

  1. def asizeof(self, *objs, **opts): 
  2.       '''Return the combined size of the given objects 
  3.          (with modified options, see method **set**). 
  4.       ''
  5.       if opts: 
  6.           self.set(**opts) 
  7.       self.exclude_refs(*objs)  # skip refs to objs 
  8.       return sum(self._sizer(o, 0, 0, None) for o in objs) 

它可以接受多個(gè)參數(shù),再用 sum() 方法合并。所以核心的計(jì)算方法其實(shí)是 _sizer()。但代碼很復(fù)雜,繞來繞去像一座迷宮:

 

  1. def _sizer(self, obj, pid, deep, sized):  # MCCABE 19 
  2.         '''Size an object, recursively. 
  3.         ''
  4.         s, f, i = 0, 0, id(obj) 
  5.         if i not in self._seen: 
  6.             self._seen[i] = 1 
  7.         elif deep or self._seen[i]: 
  8.             # skip obj if seen before 
  9.             # or if ref of a given obj 
  10.             self._seen.again(i) 
  11.             if sized: 
  12.                 s = sized(s, f, name=self._nameof(obj)) 
  13.                 self.exclude_objs(s) 
  14.             return s  # zero 
  15.         else:  # deep == seen[i] == 0 
  16.             self._seen.again(i) 
  17.         try: 
  18.             k, rs = _objkey(obj), [] 
  19.             if k in self._excl_d: 
  20.                 self._excl_d[k] += 1 
  21.             else
  22.                 v = _typedefs.get(k, None) 
  23.                 if not v:  # new typedef 
  24.                     _typedefs[k] = v = _typedef(obj, derive=self._derive_, 
  25.                                                      frames=self._frames_, 
  26.                                                       infer=self._infer_) 
  27.                 if (v.both or self._code_) and v.kind is not self._ign_d: 
  28.                     # 貓注:這里計(jì)算 flat size 
  29.                     s = f = v.flat(obj, self._mask)  # flat size 
  30.                     if self._profile: 
  31.                         # profile based on *flat* size 
  32.                         self._prof(k).update(obj, s) 
  33.                     # recurse, but not for nested modules 
  34.                     if v.refs and deep < self._limit_ \ 
  35.                               and not (deep and ismodule(obj)): 
  36.                         # add sizes of referents 
  37.                         z, d = self._sizer, deep + 1 
  38.                         if sized and deep < self._detail_: 
  39.                             # use named referents 
  40.                             self.exclude_objs(rs) 
  41.                             for o in v.refs(obj, True): 
  42.                                 if isinstance(o, _NamedRef): 
  43.                                     r = z(o.ref, i, d, sized) 
  44.                                     r.name = o.name 
  45.                                 else
  46.                                     r = z(o, i, d, sized) 
  47.                                     r.name = self._nameof(o) 
  48.                                 rs.append(r) 
  49.                                 s += r.size 
  50.                         else:  # just size and accumulate 
  51.                             for o in v.refs(obj, False): 
  52.                                 # 貓注:這里遞歸計(jì)算 item size 
  53.                                 s += z(o, i, d, None) 
  54.                         # deepest recursion reached 
  55.                         if self._depth < d: 
  56.                             self._depth = d 
  57.                 if self._stats_ and s > self._above_ > 0: 
  58.                     # rank based on *total* size 
  59.                     self._rank(k, obj, s, deep, pid) 
  60.         except RuntimeError:  # XXX RecursionLimitExceeded: 
  61.             self._missed += 1 
  62.         if not deep: 
  63.             self._total += s  # accumulate 
  64.         if sized: 
  65.             s = sized(s, f, name=self._nameof(obj), refs=rs) 
  66.             self.exclude_objs(s) 
  67.         return s 

它的核心邏輯是把每個(gè)對象的 size 分為兩部分:flat size 和 item size。

計(jì)算 flat size 的邏輯在:

 

  1. def flat(self, obj, mask=0): 
  2.         '''Return the aligned flat size
  3.         ''
  4.         s = self.base 
  5.         if self.leng and self.item > 0:  # include items 
  6.             s += self.leng(obj) * self.item 
  7.         # workaround sys.getsizeof (and numpy?) bug ... some 
  8.         # types are incorrectly sized in some Python versions 
  9.         # (note, isinstance(obj, ()) == False
  10.         # 貓注:不可 sys.getsizeof 的,則用上面邏輯,可以的,則用下面邏輯 
  11.         if not isinstance(obj, _getsizeof_excls): 
  12.             s = _getsizeof(obj, s) 
  13.         if mask:  # align 
  14.             s = (s + mask) & ~mask 
  15.         return s 

這里出現(xiàn)的 mask 是為了作字節(jié)對齊,默認(rèn)值是 7,該計(jì)算公式表示按 8 個(gè)字節(jié)對齊。對于 [1,2] 列表,會算出 (36+7)&~7=40 字節(jié)。同理,對于單個(gè)的 item,比如列表中的數(shù)字 1,sys.getsizeof(1) 等于 14,而 pympler 會算成對齊的數(shù)值 16,所以匯總起來是 40+16+16=72 字節(jié)。這就解釋了為什么 pympler 算的結(jié)果比 pysize 大。

字節(jié)對齊一般由具體的編譯器實(shí)現(xiàn),而且不同的編譯器還會有不同的策略,理論上 Python 不應(yīng)關(guān)心這么底層的細(xì)節(jié),內(nèi)置的 getsizeof() 方法就沒有考慮字節(jié)對齊。

在不考慮其它 edge cases 的情況下,可以認(rèn)為 pympler 是在 getsizeof() 的基礎(chǔ)上,既考慮了遍歷取引用對象的 size,又考慮到了實(shí)際存儲時(shí)的字節(jié)對齊問題,所以它會顯得更加貼近現(xiàn)實(shí)。

4、小結(jié)

getsizeof() 方法的問題是顯而易見的,我創(chuàng)造了一個(gè)“淺計(jì)算”概念給它。這個(gè)概念借鑒自 copy() 方法的“淺拷貝”,同時(shí)對應(yīng)于 deepcopy() “深拷貝”,我們還能推理出一個(gè)“深計(jì)算”。

前面展示了兩個(gè)試圖實(shí)現(xiàn)“深計(jì)算”的項(xiàng)目(pysize+pympler),兩者在淺計(jì)算的基礎(chǔ)上,深入地求解引用對象的大小。pympler 項(xiàng)目的完整度較高,代碼中有很多細(xì)節(jié)上的設(shè)計(jì),比如字節(jié)對齊。

Python 官方團(tuán)隊(duì)當(dāng)然也知道 getsizeof() 方法的局限性,他們甚至在文檔中加了一個(gè)鏈接 [3],指向了一份實(shí)現(xiàn)深計(jì)算的示例代碼。那份代碼比 pysize 還要簡單(沒有考慮類對象的情況)。

未來 Python 中是否會出現(xiàn)深計(jì)算的方法,假設(shè)命名為 getdeepsizeof() 呢?這不得而知了。

本文的目的是加深對 getsizeof() 方法的理解,區(qū)分淺計(jì)算與深計(jì)算,分析兩個(gè)深計(jì)算項(xiàng)目的實(shí)現(xiàn)思路,指出幾個(gè)值得注意的問題。

讀完這里,希望你也能有所收獲。若有什么想法,歡迎一起交流。

責(zé)任編輯:華軒 來源: Python貓
相關(guān)推薦

2021-01-25 17:24:13

云計(jì)算云服務(wù)器云安全

2011-07-27 10:53:47

交換機(jī)

2012-05-22 09:41:12

Python

2010-02-03 16:32:13

2018-04-17 11:30:03

云計(jì)算IaaS公共云

2018-08-10 07:04:47

數(shù)據(jù)中心運(yùn)維云計(jì)算

2021-01-13 10:33:57

云計(jì)算云遷移云平臺

2020-08-17 08:00:54

計(jì)算機(jī)IT互聯(lián)網(wǎng)

2010-08-23 09:35:12

云計(jì)算SaaS

2011-07-15 08:52:45

UML工具

2010-08-30 09:22:13

DIV高度自適應(yīng)

2017-11-07 21:05:43

機(jī)房配電柜配電箱

2012-09-20 15:11:31

Unix服務(wù)器

2010-06-04 14:42:25

2011-07-08 14:09:51

iPhone UI

2009-03-19 18:36:49

虛擬化Vmwareesx

2024-10-08 09:43:44

golang高并發(fā)加鎖事務(wù)

2010-01-06 16:41:40

解析JSON

2010-01-07 09:44:30

學(xué)習(xí)JavaScrip

2023-12-28 09:54:22

Java內(nèi)存開發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號