日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

c python 内存冲突_Python在计算内存时应该注意的问题?

發布時間:2025/3/15 python 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c python 内存冲突_Python在计算内存时应该注意的问题? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

文檔中關于這個方法的介紹有兩層意思:

該方法用于獲取一個對象的字節大小(bytes)

它只計算直接占用的內存,而不計算對象內所引用對象的內存

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

例如列表 [1,2],getsizeof() 不會把列表內兩個元素的實際大小算上,而只是計算了對它們的引用。

舉一個形象的例子,我們把列表想象成一個箱子,把它存儲的對象想象成一個個球,現在箱子里有兩張紙條,寫上了球 1 和球 2 的地址(球不在箱子里),getsizeof() 只是把整個箱子稱重(含紙條),而沒有根據紙條上地址,找到兩個球一起稱重。

1、計算的是什么?

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

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

如果不使用引用方式,而是直接把子列表寫進去,例如 “d = [[1,2],[1,2,3,4,5]]”,這樣計算 d 列表的結果也還是 36,因為子列表是獨立的對象,在 d 列表中存儲的是它們的 id。

也就是說:getsizeof() 方法在計算列表大小時,其結果跟元素個數相關,但跟元素本身的大小無關。

下面再看看字典的例子:

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

2、“淺計算”與其它問題

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

“淺計算”不關心真實的對象,所以其計算結果只是一個假象。這是一個值得注意的問題,但是注意到這點還不夠,我們還可以發散地思考如下的問題:

“淺計算”方法的底層實現是怎樣的?

為什么 getsizeof() 會采用“淺計算”的方法?

關于第一個問題,getsizeof(x) 方法實際會調用 x 對象的__sizeof__() 魔術方法,對于內置對象來說,這個方法是通過 CPython 解釋器實現的。

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

/*longobject.c*/

static Py_ssize_t

int___sizeof___impl(PyObject *self)

{

Py_ssize_t res;

res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit);

return res;

}

我看不懂這段代碼,但是可以知道的是,它在計算 Python 對象的大小時,只跟該對象的結構體的屬性相關,而沒有進一步作“深度計算”。

對于 CPython 的這種實現,我們可以注意到兩個層面上的區別:

字節增大:int 類型在 C 語言中只占到 4 個字節,但是在 Python 中,int 其實是被封裝成了一個對象,所以在計算其大小時,會包含對象結構體的大小。在 32 位解釋器中,getsizeof(1) 的結果是 14 個字節,比數字本身的 4 字節增大了。

字節減少:對于相對復雜的對象,例如列表和字典,這套計算機制由于沒有累加內部元素的占用量,就會出現比真實占用內存小的結果。

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

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

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

3、“深計算”與其它問題

與“淺計算”相對應,我們可以定義出一種“深計算”。對于前面的兩個例子,“深計算”應該遍歷每個內部元素以及可能的子元素,累加計算它們的字節,最后算出總的內存大小。

那么,我們應該注意的問題有:

是否存在“深計算”的方法/實現方案?

實現“深計算”時應該注意什么?

Stackoverflow 網站上有個年代久遠的問題“How do I determine the size of an object in Python?” [2],實際上問的就是如何實現“深計算”的問題。

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

對于前面的兩個例子,我們可以拿這兩個項目分別測試一下:

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

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

64

118

190

206

300281

30281

可以看出,它比 pympler 計算的結果略小。就兩個項目的完整度、使用量與社區貢獻者規模來看,pympler 的結果似乎更為可信。

那么,它們分別是怎么實現的呢?那微小的差異是怎么導致的?從它們的實現方案中,我們可以學習到什么呢?

pysize 項目很簡單,只有一個核心方法:

def get_size(obj, seen=None):

"""Recursively finds size of objects in bytes"""

size = sys.getsizeof(obj)

if seen is None:

seen = set()

obj_id = id(obj)

if obj_id in seen:

return 0

# Important mark as seen *before* entering recursion to gracefully handle

# self-referential objects

seen.add(obj_id)

if hasattr(obj, '__dict__'):

for cls in obj.__class__.__mro__:

if '__dict__' in cls.__dict__:

d = cls.__dict__['__dict__']

if inspect.isgetsetdescriptor(d) or inspect.ismemberdescriptor(d):

size += get_size(obj.__dict__, seen)

break

if isinstance(obj, dict):

size += sum((get_size(v, seen) for v in obj.values()))

size += sum((get_size(k, seen) for k in obj.keys()))

elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):

size += sum((get_size(i, seen) for i in obj))

if hasattr(obj, '__slots__'): # can have __slots__ with __dict__

size += sum(get_size(getattr(obj, s), seen) for s in obj.__slots__ if hasattr(obj, s))

return size

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

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

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

def asizeof(self, *objs, **opts):

'''Return the combined size of the given objects

(with modified options, see method **set**).

'''

if opts:

self.set(**opts)

self.exclude_refs(*objs) # skip refs to objs

return sum(self._sizer(o, 0, 0, None) for o in objs)

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

def _sizer(self, obj, pid, deep, sized): # MCCABE 19

'''Size an object, recursively.

'''

s, f, i = 0, 0, id(obj)

if i not in self._seen:

self._seen[i] = 1

elif deep or self._seen[i]:

# skip obj if seen before

# or if ref of a given obj

self._seen.again(i)

if sized:

s = sized(s, f, name=self._nameof(obj))

self.exclude_objs(s)

return s # zero

else: # deep == seen[i] == 0

self._seen.again(i)

try:

k, rs = _objkey(obj), []

if k in self._excl_d:

self._excl_d[k] += 1

else:

v = _typedefs.get(k, None)

if not v: # new typedef

_typedefs[k] = v = _typedef(obj, derive=self._derive_,

frames=self._frames_,

infer=self._infer_)

if (v.both or self._code_) and v.kind is not self._ign_d:

# 貓注:這里計算 flat size

s = f = v.flat(obj, self._mask) # flat size

if self._profile:

# profile based on *flat* size

self._prof(k).update(obj, s)

# recurse, but not for nested modules

if v.refs and deep < self._limit_ \

and not (deep and ismodule(obj)):

# add sizes of referents

z, d = self._sizer, deep + 1

if sized and deep < self._detail_:

# use named referents

self.exclude_objs(rs)

for o in v.refs(obj, True):

if isinstance(o, _NamedRef):

r = z(o.ref, i, d, sized)

r.name = o.name

else:

r = z(o, i, d, sized)

r.name = self._nameof(o)

rs.append(r)

s += r.size

else: # just size and accumulate

for o in v.refs(obj, False):

# 貓注:這里遞歸計算 item size

s += z(o, i, d, None)

# deepest recursion reached

if self._depth < d:

self._depth = d

if self._stats_ and s > self._above_ > 0:

# rank based on *total* size

self._rank(k, obj, s, deep, pid)

except RuntimeError: # XXX RecursionLimitExceeded:

self._missed += 1

if not deep:

self._total += s # accumulate

if sized:

s = sized(s, f, name=self._nameof(obj), refs=rs)

self.exclude_objs(s)

return s

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

計算 flat size 的邏輯在:

def flat(self, obj, mask=0):

'''Return the aligned flat size.

'''

s = self.base

if self.leng and self.item > 0: # include items

s += self.leng(obj) * self.item

# workaround sys.getsizeof (and numpy?) bug ... some

# types are incorrectly sized in some Python versions

# (note, isinstance(obj, ()) == False)

# 貓注:不可 sys.getsizeof 的,則用上面邏輯,可以的,則用下面邏輯

if not isinstance(obj, _getsizeof_excls):

s = _getsizeof(obj, s)

if mask: # align

s = (s + mask) & ~mask

return s

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

字節對齊一般由具體的編譯器實現,而且不同的編譯器還會有不同的策略,理論上 Python 不應關心這么底層的細節,內置的 getsizeof() 方法就沒有考慮字節對齊。

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

4、小結

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

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

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

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

本文的目的是加深對 getsizeof() 方法的理解,區分淺計算與深計算,分析兩個深計算項目的實現思路,指出幾個值得注意的問題。

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

相關鏈接

公眾號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

總結

以上是生活随笔為你收集整理的c python 内存冲突_Python在计算内存时应该注意的问题?的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。