我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Python在计算内存时应该注意哪些问题

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Python在计算内存时应该注意哪些问题

本篇内容介绍了“Python在计算内存时应该注意哪些问题”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

计算的是什么?

我们先来看看列表对象的情况:

Python在计算内存时应该注意哪些问题

如图所示,单独计算 a 和 b 列表的结果是 36 和 48,然后把它们作为 c 列表的子元素时,该列表的计算结果却仅仅才 36。(PS:我用的是 32  位解释器)

如果不使用引用方式,而是直接把子列表写进去,例如 “d = [[1,2],[1,2,3,4,5]]”,这样计算 d 列表的结果也还是  36,因为子列表是独立的对象,在 d 列表中存储的是它们的 id。

也就是说:getsizeof() 方法在计算列表大小时,其结果跟元素个数相关,但跟元素本身的大小无关。

下面再看看字典的例子:

Python在计算内存时应该注意哪些问题

明显可以看出,三个字典实际占用的全部内存不可能相等,但是 getsizeof()  方法给出的结果却相同,这意味着它只关心键的数量,而不关心实际的键值对是什么内容,情况跟列表相似。

2、“浅计算”与其它问题

有个概念叫“浅拷贝”,指的是 copy() 方法只拷贝引用对象的内存地址,而非实际的引用对象。类比于这个概念,我们可以认为  getsizeof() 是一种“浅计算”。

“浅计算”不关心真实的对象,所以其计算结果只是一个假象。这是一个值得注意的问题,但是注意到这点还不够,我们还可以发散地思考如下的问题:

  • “浅计算”方法的底层实现是怎样的?

  • 为什么 getsizeof() 会采用“浅计算”的方法?

关于第一个问题,getsizeof(x) 方法实际会调用 x 对象的__sizeof__() 魔术方法,对于内置对象来说,这个方法是通过 CPython  解释器实现的。

我查到这篇文章《Python中对象的内存使用(一)》,它分析了 CPython 源码,最终定位到的核心代码是这一段:

  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 上获取到其源码。

对于前面的两个例子,我们可以拿这两个项目分别测试一下:

Python在计算内存时应该注意哪些问题

单看数值的话,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,又考虑到了实际存储时的字节对齐问题,所以它会显得更加贴近现实。

“Python在计算内存时应该注意哪些问题”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Python在计算内存时应该注意哪些问题

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Python在计算内存时应该注意哪些问题

本篇内容介绍了“Python在计算内存时应该注意哪些问题”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1、计算的是什么?我们先来看看列表对象
2023-06-16

使用FlexaddChild()方法时应该注意的问题有哪些

小编给大家分享一下使用FlexaddChild()方法时应该注意的问题有哪些,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!FlexaddChild()方法注意事项在FlexApplication里,是不能直接用Flexad
2023-06-17

hashMap扩容时应该注意的死循环问题有哪些

小编给大家分享一下hashMap扩容时应该注意的死循环问题有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!hashMap死循环1.原因: jdk1.7时使用头
2023-06-15

制作404错误页面时有哪些应该注意的问题

这篇文章主要介绍“制作404错误页面时有哪些应该注意的问题”,在日常操作中,相信很多人在制作404错误页面时有哪些应该注意的问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”制作404错误页面时有哪些应该注意
2023-06-08

制作404错误页面时应该注意的问题有哪些

本篇文章为大家展示了制作404错误页面时应该注意的问题有哪些,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。在做网站优化时我们要学会利用错误页面,例如我们网站可能出现的404错误页面,下面就给大家讲一
2023-06-10

在python中使用os.remove()时需要注意哪些问题

在python中使用os.remove()时需要注意哪些问题?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。python可以做什么Python是一种编程语言,内置
2023-06-14

在python中使用类属性时需要注意哪些问题

本篇文章给大家分享的是有关在python中使用类属性时需要注意哪些问题,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Python主要用来做什么Python主要应用于:1、Web
2023-06-14

SEO优化网站首页设计优化时应注意哪些问题

本篇内容主要讲解“SEO优化网站首页设计优化时应注意哪些问题”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“SEO优化网站首页设计优化时应注意哪些问题”吧!  良好的页面布局及代码优化对用户体验的
2023-06-10

在python中使用匿名函数时需要注意哪些问题

在python中使用匿名函数时需要注意哪些问题?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。python可以做什么Python是一种编程语言,内置了许多有效的工
2023-06-14

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录