在Python中如何确定对象的大小?

1037

我如何在Python中获取对象占用的内存大小?

16个回答

931

只需使用 sys.getsizeof 函数,该函数定义在 sys 模块中。

sys.getsizeof(object[, default])

返回对象的字节大小。对象可以是任何类型的对象。所有内置对象都将返回正确的结果,但对于第三方扩展来说,这并不一定是正确的,因为这取决于实现。

只考虑直接归因于该对象的内存消耗,而不考虑它所引用的对象的内存消耗。

default 参数允许定义一个值,如果对象类型没有提供检索大小的方法并导致 TypeError,则返回该值。

getsizeof 调用对象的 __sizeof__ 方法,并在对象由垃圾收集器管理时增加额外的垃圾收集器开销。

有关递归计算容器及其所有内容的大小示例,请参见 递归 sizeof 示例

Python 3.0 中的使用示例:

>>> import sys
>>> x = 2
>>> sys.getsizeof(x)
24
>>> sys.getsizeof(sys.getsizeof)
32
>>> sys.getsizeof('this')
38
>>> sys.getsizeof('this also')
48

如果你使用的是 Python < 2.6,并且没有 sys.getsizeof 函数,那么你可以使用这个广泛的模块来代替。尽管我从未使用过它。


337
请在免责声明中添加以下内容:此声明不适用于嵌套对象、嵌套字典或列表中的字典等情况。 - JohnnyM
14
@ChaimG,这是因为每个对象只使用32个字节!其余引用其他对象。如果您想计算引用的对象,您必须为您的类定义__sizeof__方法。Python内置的dict类已经定义了它,这就是为什么在使用dict类型的对象时可以得到正确的结果。 - nosklo
47
免责声明和除外条款几乎覆盖了所有使用情况,使得“getsizeof”函数在默认情况下的价值很小。 - Robino
17
为什么整数2要占用24个字节的存储空间? - Saher Ahwal
15
@SaherAhwal 这不仅仅是一个整数,它是一个带有方法、属性和地址的完整对象。 - nosklo
显示剩余2条评论

606

如何在Python中确定对象的大小?

答案“只需使用sys.getsizeof”并不完整。

该答案确实适用于直接内置对象,但它没有考虑这些对象可能包含什么类型的内容,特别是自定义对象、元组、列表、字典和集合。它们可以相互包含,以及数字、字符串和其他对象。

更完整的答案

使用来自Anaconda分发的64位Python 3.6和sys.getsizeof,我已确定以下对象的最小大小,并注意到集合和字典预先分配空间,因此空集合在一定数量之后不会再次增长(这可能因语言实现而异):

Python 3:

Empty
Bytes  type        scaling notes
28     int         +4 bytes about every 30 powers of 2
37     bytes       +1 byte per additional byte
49     str         +1-4 per additional character (depending on max width)
48     tuple       +8 per additional item
64     list        +8 for each additional
224    set         5th increases to 736; 21nd, 2272; 85th, 8416; 341, 32992
240    dict        6th increases to 368; 22nd, 1184; 43rd, 2280; 86th, 4704; 171st, 9320
136    func def    does not include default args and other attrs
1056   class def   no slots 
56     class inst  has a __dict__ attr, same scaling as dict above
888    class def   with slots
16     __slots__   seems to store in mutable tuple-like structure
                   first slot grows to 48, and so on.

你如何解释这个问题?假设你有一个包含10个项目的集合。如果每个项目都是100字节,整个数据结构有多大?集合本身大小为736字节,因为它已经增加了一次大小。然后您添加项目的大小,总共是1736字节。

一些关于函数和类定义的注意事项:

请注意,每个类定义都有一个代理__dict__(48字节)结构用于类属性。每个插槽在类定义中都有一个描述符(例如property)。

插槽实例从其第一个元素开始有48字节,并且每个额外元素增加8字节。只有空插槽对象具有16字节,没有数据的实例几乎没有意义。

此外,每个函数定义都有代码对象,可能有文档字符串和其他可能的属性,甚至有一个__dict__

还要注意,我们使用sys.getsizeof(),因为我们关心边际空间使用情况,其中包括对象的垃圾回收开销,来自文档:

getsizeof() 调用对象的 __sizeof__ 方法,并在对象由垃圾回收器管理时添加额外的垃圾回收器开销。

还要注意,调整列表大小(例如重复附加到它们)会导致它们预分配空间,与集合和字典类似。来自 listobj.c source code 的源代码:

    /* This over-allocates proportional to the list size, making room
     * for additional growth.  The over-allocation is mild, but is
     * enough to give linear-time amortized behavior over a long
     * sequence of appends() in the presence of a poorly-performing
     * system realloc().
     * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
     * Note: new_allocated won't overflow because the largest possible value
     *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
     */
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

历史数据

Python 2.7分析,使用guppy.hpysys.getsizeof进行确认:

Bytes  type        empty + scaling notes
24     int         NA
28     long        NA
37     str         + 1 byte per additional character
52     unicode     + 4 bytes per additional character
56     tuple       + 8 bytes per additional item
72     list        + 32 for first, 8 for each additional
232    set         sixth item increases to 744; 22nd, 2280; 86th, 8424
280    dict        sixth item increases to 1048; 22nd, 3352; 86th, 12568 *
120    func def    does not include default args and other attrs
64     class inst  has a __dict__ attr, same scaling as dict above
16     __slots__   class with slots has no dict, seems to store in 
                    mutable tuple-like structure.
904    class def   has a proxy __dict__ structure for class attrs
104    old class   makes sense, less stuff, has real dict though.

请注意,字典(但不包括集合)在Python 3.6中采用了更紧凑的表示形式。
我认为,在64位机器上,每个附加项引用8个字节是有意义的。这8个字节指向所包含项目在内存中的位置。如果我没记错的话,在Python 2中,4个字节是固定宽度的Unicode,但是在Python 3中,str成为了最大字符宽度相等的Unicode。
有关slots的更多信息,请参见此答案。
一个更完整的函数
我们希望编写一个函数,可以搜索列表、元组、集合、字典、obj.__dict__和obj.__slots__中的元素,以及其他一些我们可能尚未考虑到的内容。
我们希望使用 gc.get_referents 进行搜索,因为它可以在 C 级别上运行(使其非常快)。缺点是 get_referents 可能会返回冗余成员,因此我们需要确保不重复计数。
类、模块和函数都是单例 - 它们在内存中存在一次。我们对它们的大小不太感兴趣,因为我们无法做太多关于它们的事情 - 它们是程序的一部分。因此,如果它们被引用,我们将避免计算它们的数量。
我们将使用类型黑名单,以便在计算大小时不包括整个程序。
import sys
from types import ModuleType, FunctionType
from gc import get_referents

# Custom objects know their class.
# Function objects seem to know way too much, including modules.
# Exclude modules as well.
BLACKLIST = type, ModuleType, FunctionType


def getsize(obj):
    """sum size of object & members."""
    if isinstance(obj, BLACKLIST):
        raise TypeError('getsize() does not take argument of type: '+ str(type(obj)))
    seen_ids = set()
    size = 0
    objects = [obj]
    while objects:
        need_referents = []
        for obj in objects:
            if not isinstance(obj, BLACKLIST) and id(obj) not in seen_ids:
                seen_ids.add(id(obj))
                size += sys.getsizeof(obj)
                need_referents.append(obj)
        objects = get_referents(*need_referents)
    return size

与下面的白名单函数相比,大多数对象知道如何遍历自身以进行垃圾回收(当我们想知道某些对象在内存中的昂贵程度时,这就是我们要寻找的内容。此功能由gc.get_referents使用)。然而,如果我们不小心,这个度量将比我们预期的范围要大得多。
例如,函数对它们所创建的模块了解得很多。
另一个对比点是,在字典中作为键的字符串通常被内部化,因此它们不会重复。检查id(key)也将使我们避免计算重复项,这是我们在下一节中要做的。黑名单解决方案完全跳过了作为字符串的键的计数。
白名单类型,递归访问者
为了覆盖大多数这些类型,而不是依赖于gc模块,我编写了这个递归函数来尝试估计大多数Python对象的大小,包括大多数内置对象、集合模块中的类型和自定义类型(有槽和无槽)。
这种函数可以更细致地控制我们要计算内存使用情况的类型,但可能会漏掉重要的类型:
import sys
from numbers import Number
from collections import deque
from collections.abc import Set, Mapping


ZERO_DEPTH_BASES = (str, bytes, Number, range, bytearray)


def getsize(obj_0):
    """Recursively iterate to sum size of object & members."""
    _seen_ids = set()
    def inner(obj):
        obj_id = id(obj)
        if obj_id in _seen_ids:
            return 0
        _seen_ids.add(obj_id)
        size = sys.getsizeof(obj)
        if isinstance(obj, ZERO_DEPTH_BASES):
            pass # bypass remaining control flow and return
        elif isinstance(obj, (tuple, list, Set, deque)):
            size += sum(inner(i) for i in obj)
        elif isinstance(obj, Mapping) or hasattr(obj, 'items'):
            size += sum(inner(k) + inner(v) for k, v in getattr(obj, 'items')())
        # Check for custom object instances - may subclass above too
        if hasattr(obj, '__dict__'):
            size += inner(vars(obj))
        if hasattr(obj, '__slots__'): # can have __slots__ with __dict__
            size += sum(inner(getattr(obj, s)) for s in obj.__slots__ if hasattr(obj, s))
        return size
    return inner(obj_0)

我比较随意地测试了一下(其实我应该写单元测试的):

>>> getsize(['a', tuple('bcd'), Foo()])
344
>>> getsize(Foo())
16
>>> getsize(tuple('bcd'))
194
>>> getsize(['a', tuple('bcd'), Foo(), {'foo': 'bar', 'baz': 'bar'}])
752
>>> getsize({'foo': 'bar', 'baz': 'bar'})
400
>>> getsize({})
280
>>> getsize({'foo':'bar'})
360
>>> getsize('foo')
40
>>> class Bar():
...     def baz():
...         pass
>>> getsize(Bar())
352
>>> getsize(Bar().__dict__)
280
>>> sys.getsizeof(Bar())
72
>>> getsize(Bar.__dict__)
872
>>> sys.getsizeof(Bar.__dict__)
280

这个实现在类定义和函数定义中会出现问题,因为我们没有追踪它们的所有属性,但是由于它们应该只存在于进程的内存中一次,因此它们的大小并不太重要。


2
任何在C中实现的自定义对象,如果没有正确实现__sizeof__,将无法与sys.getsizeof一起使用。这并没有得到很好的记录,因为它被认为是一个实现细节(请参见https://bugs.python.org/issue15436)。不要期望此函数覆盖所有情况-根据需要修改它以最适合您的用例。 - Russia Must Remove Putin
包含非ASCII字符的str具有更高的开销,例如,sys.getsizeof('я')为76,而sys.getsizeof('')为80。 - user3064538
我敢打赌,第二种情况下终止空值占用了4个字节,这就是2个字节的差异。 - user3064538

174

可以使用Pympler包中的asizeof模块来实现此功能。

使用方法如下:

from pympler import asizeof
asizeof.asizeof(my_object)

sys.getsizeof不同,它适用于您自己创建的对象。甚至可以与numpy一起使用。
>>> asizeof.asizeof(tuple('bcd'))
200
>>> asizeof.asizeof({'foo': 'bar', 'baz': 'bar'})
400
>>> asizeof.asizeof({})
280
>>> asizeof.asizeof({'foo':'bar'})
360
>>> asizeof.asizeof('foo')
40
>>> asizeof.asizeof(Bar())
352
>>> asizeof.asizeof(Bar().__dict__)
280
>>> A = rand(10)
>>> B = rand(10000)
>>> asizeof.asizeof(A)
176
>>> asizeof.asizeof(B)
80096

如果您需要对实时数据进行其他视图,Pympler的模块muppy用于在线监控Python应用程序, 而模块Class Tracker则提供对所选Python对象的寿命进行离线分析。

@serv-inc:如果需要我测试,请告诉我,否则我的程序似乎没有内存泄漏,所以没有紧急需要。 - James Hirschorn
2
@ihavenoidea: 字节(想象一下每个Python对象占用280 kbytes) - serv-inc

88
对于NumPy数组,getsizeof方法不起作用——对我来说,它总是以某种原因返回40:
from pylab import *
from sys import getsizeof
A = rand(10)
B = rand(10000)

然后(在IPython中):

In [64]: getsizeof(A)
Out[64]: 40

In [65]: getsizeof(B)
Out[65]: 40

然而,令人欣慰的是:

In [66]: A.nbytes
Out[66]: 80

In [67]: B.nbytes
Out[67]: 80000

33
所有内置对象都将返回正确的结果,但对于第三方扩展来说,这并不一定成立,因为它是实现特定的。http://docs.python.org/library/sys.html#sys.getsizeof - warvariuc
39
如果您正在使用numpy数组(http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html),那么可以使用属性“ndarray.nbytes”来评估其在内存中的大小。 - glarrain
20
我猜测40字节是正确的,但getsizeof()只会返回对象的大小(数组头部),而不是其中的数据。对于Python容器也是一样,其中sys.getsizeof([1,2,4]) == sys.getsizeof([1,123**456,4]) == 48,而sys.getsizeof(123**456) = 436 - yota
4
似乎 getsizeof() 函数在某个时候被更改以返回预期值。 - dshin

84
您可以将对象序列化为导出一个与对象大小密切相关的度量:
import pickle

## let o be the object whose size you want to measure
size_estimate = len(pickle.dumps(o))

如果您想测量无法被pickle(例如由于lambda表达式)的对象,dill或cloudpickle可能是一种解决方案。


3
我觉得这是最简单、最有用的,特别是当我需要序列化 Python 对象(例如多进程等)时,我最关心 Python 对象的大小。 - StatsNoob
1
当numpy切片占用内存时,代码无法正常工作。例如: import numpy as np; a = np.arange(100000000); b = a[2:4]; del a; len(pickle.dumps(b)) # 150, but the array is 100MB or more depending on the dtype - Torben545
另一种情况是无法正常工作的:TypeError: cannot pickle '_thread.lock' object -- 将尝试使用建议的 dill/cloudpickle - Avi Vajpeyi
到目前为止,最佳答案! - Yahya
这里的单位是什么?字节吗? - undefined

36

如果您不想包含链接(嵌套)对象的大小,请使用sys.getsizeof()

然而,如果您想计算嵌套在列表、字典、集合、元组中的子对象数量 - 通常这才是您要查找的内容 - 请使用递归的深度 sizeof()函数,如下所示:

import sys
def sizeof(obj):
    size = sys.getsizeof(obj)
    if isinstance(obj, dict): return size + sum(map(sizeof, obj.keys())) + sum(map(sizeof, obj.values()))
    if isinstance(obj, (list, tuple, set, frozenset)): return size + sum(map(sizeof, obj))
    return size

您还可以在 nifty 工具箱中找到此功能,与许多其他有用的一行代码一起使用:

https://github.com/mwojnars/nifty/blob/master/util.py


2
当numpy切片占用内存时,代码无法正常工作。例如: import numpy as np; a = np.arange(100000000); b = a[2:4]; del a; len(pickle.dumps(b)) # 150, but the array is 100MB or more depending on the dtype - Torben545
1
唯一正确的答案,已点赞。 - Arka Mukherjee

28

Python 3.8 (2019年第一季度)将更改sys.getsizeof的一些结果,如Raymond Hettinger在此处宣布的那样:

64位构建的Python容器将减少8个字节。

tuple ()  48 -> 40       
list  []  64 ->56
set()    224 -> 216
dict  {} 240 -> 232

这是在问题33597Inada Naoki (methane)关于Compact PyGC_Head的工作之后,以及PR 7043之后提出的想法。 这个想法将PyGC_Head的大小减小到两个字。 当前,PyGC_Head占用三个字; gc_prev, gc_next, 和 gc_refcntgc_refcnt用于收集时的试删除。 gc_prev用于跟踪和取消跟踪。
因此,如果我们可以在试删除时避免跟踪/取消跟踪,则gc_prevgc_refcnt可以共享相同的内存空间。
参见提交d5c875b
去除了一个Py_ssize_t成员从PyGC_Head中。 所有GC跟踪的对象(例如元组、列表、字典)的大小减小了4或8个字节。

16

根据计数方法的不同,这可能比看起来更加复杂。例如,如果您有一个int列表,您是想要包含指向实际数据的引用的列表大小吗?(即仅限于列表,而不是其中包含的内容),还是您想要包含所指向的实际数据,在这种情况下,您需要处理重复引用,并且如何防止两个对象包含对同一对象的引用时重复计数。

您可能希望查看其中一个Python内存分析工具,例如pysizer,以查看它们是否符合您的需求。


12

我自己遇到过这个问题很多次,受 @aaron-hall 的答案启发,我写了一个小函数和测试来实现我原本期望 sys.getsizeof 能做到的事情:

https://github.com/bosswissam/pysize

如果你对背景感兴趣,在这里可以找到。

编辑: 以下附上代码以供方便参考,如需查看最新代码,请查看 github 链接。

    import sys

    def get_size(obj, seen=None):
        """Recursively finds size of objects"""
        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 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, '__dict__'):
            size += get_size(obj.__dict__, seen)
        elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
            size += sum([get_size(i, seen) for i in obj])
        return size

在 pd.Series 上出现“TypeError: 'Int64Index' object is not callable”崩溃 - banderlog013

7

这是我根据之前的答案编写的一个快速脚本,用于列出所有变量的大小

for i in dir():
    print (i, sys.getsizeof(eval(i)) )

这并没有错,只是有歧义。sys.getsizeof将始终返回所需的值,因此没有必要使用try..except失去性能。 - der_fenix
哦,那是一个很好的观点,我没有想到 - 目前的代码只是按照时间顺序编写 - 首先我知道了numpy(因此nbytes),然后我查找了更通用的解决方案。谢谢您的解释 /\ - alexey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接