当你导入pandas时,它会执行大量的NumPy操作,包括调用
UNICODE_setitem
来处理所有单个ASCII字母字符串,在其他地方也可能对单个ASCII数字字符串执行类似的操作。
这个NumPy函数调用了已弃用的C API
PyUnicode_AsUnicode
。
当你在CPython 3.3+中调用它时,它会将
wchar_t *
表示形式缓存在字符串内部结构的
wstr
成员中,作为两个wchar_t值
w'a'
和
'\0'
,这在32位
wchar_t
构建的Python中占用8个字节。并且
str.__size__
将考虑到这一点。
因此,所有ASCII字母和数字的单字符变量字符串会增加8个字节大小。
首先,我们知道这显然发生在
import pandas
上(参见
Brad Solomon's answer)。它可能会发生在
np.set_printoptions(precision=4, threshold=625, edgeitems=10)
上(miradulo曾在
ShadowRanger's answer中发表过评论,但后来删除了),但绝不会发生在
import numpy
上。
其次,我们知道它发生在
'a'
上,但其他单字符字符串呢?
为了验证前者并测试后者,我运行了这段代码:
import sys
strings = [chr(i) for i in (0, 10, 17, 32, 34, 47, 48, 57, 58, 64, 65, 90, 91, 96, 97, 102, 103, 122, 123, 130, 0x0222, 0x12345)]
sizes = {c: sys.getsizeof(c) for c in strings}
print(sizes)
import numpy as np
sizes = {c: sys.getsizeof(c) for c in strings}
print(sizes)
np.set_printoptions(precision=4, threshold=625, edgeitems=10)
sizes = {c: sys.getsizeof(c) for c in strings}
print(sizes)
import pandas
sizes = {c: sys.getsizeof(c) for c in strings}
print(sizes)
在多个CPython安装中(但所有64位CPython 3.4或更高版本在Linux或macOS上),我得到了相同的结果:
{'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80}
{'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80}
{'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80}
{'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 58, '9': 58, ':': 50, '@': 50, 'A': 58, 'Z': 58, '[': 50, '`': 50, 'a': 58, 'f': 58, 'g': 58, 'z': 58, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80}
所以,
import numpy
不会改变任何东西,
set_printoptions
也是如此(很可能是为什么 miradulo 删除了评论……),但
import pandas
会有影响。
而且,它显然只影响 ASCII 数字和字母,不影响其他字符。
另外,如果你把所有的
print
改成
print(sizes.values())
,这样字符串就不会被编码输出,你会得到相同的结果,这意味着要么不是关于缓存 UTF-8,要么即使我们不强制使用它,它也总是发生。
明显的可能性是Pandas正在调用
遗留的PyUnicode
API之一,以生成所有ASCII数字和字母的单字符字符串。所以这些字符串最终不是以紧凑的ASCII格式,而是以遗留的格式呈现,对吗?(有关此含义的详细信息,请参见
源代码中的注释。)
不是的。使用我
superhackyinternals
的代码,我们可以看到它仍然是紧凑的ASCII格式:
import ctypes
import sys
from internals import PyUnicodeObject
s = 'a'
print(sys.getsizeof(s))
ps = PyUnicodeObject.from_address(s)
print(ps, ps.kind, ps.length, ps.interned, ps.ascii, ps.compact, ps.ready)
addr = id(s) + PyUnicodeObject.utf8_length.offset
buf = (ctypes.c_char * 2).from_address(addr)
print(addr, bytes(buf))
import pandas
print(sys.getsizeof(s))
s = 'a'
ps = PyUnicodeObject.from_address(s)
print(ps, ps.kind, ps.length, ps.interned, ps.ascii, ps.compact, ps.ready)
addr = id(s) + PyUnicodeObject.utf8_length.offset
buf = (ctypes.c_char * 2).from_address(addr)
print(addr, bytes(buf))
我们可以看到Pandas将大小从50更改为58,但字段仍然是:
<__main__.PyUnicodeObject object at 0x101bbae18> 1 1 1 1 1 1
换句话说,它是1BYTE_KIND
,长度为1,可回收的,ASCII编码,紧凑且就绪。
但是,如果你看一下ps.wstr
,在Pandas之前它是一个空指针,而在Pandas之后它是指向wchar_t
字符串w"a\0"
的指针。并且str.__sizeof__
将考虑到wstr
的大小。
因此,问题是,如何得到一个具有
wstr
值的 ASCII 紧凑字符串?
简单:您调用
PyUnicode_AsUnicode
(或者其他访问 3.2 风格本地
wchar_t *
内部存储的已弃用函数或宏)。该本地内部存储在 3.3+ 中实际上并不存在。因此,为了向后兼容,这些调用通过即时创建该存储空间、将其放置在
wstr
成员上,并调用相应的
PyUnicode_AsUCS[24]
函数对其进行解码来处理。(除非您处理的紧凑字符串的种类恰好与
wchar_t
宽度匹配,在这种情况下,
wstr
只是指向本地存储的指针。)
您希望
str.__sizeof__
最好包括该额外存储空间,而
从源代码中,您可以看到它确实包括。
让我们验证一下:
import ctypes
import sys
s = 'a'
print(sys.getsizeof(s))
ctypes.pythonapi.PyUnicode_AsUnicode.argtypes = [ctypes.py_object]
ctypes.pythonapi.PyUnicode_AsUnicode.restype = ctypes.c_wchar_p
print(ctypes.pythonapi.PyUnicode_AsUnicode(s))
print(sys.getsizeof(s))
塔达,我们的50变成了58。
那么,你如何确定这个调用发生在哪里?
实际上,在Pandas和Numpy中有大量对PyUnicode_AsUnicode
、PyUnicode_AS_UNICODE
宏以及其他调用它们的函数的调用。因此,我在lldb中运行Python,并将断点附加到PyUnicode_AsUnicode
上,并使用一个跳过脚本,如果调用堆栈帧与上次相同,则跳过。
前几个调用涉及日期时间格式。然后有一个单字母调用。堆栈框架为:
multiarray.cpython-36m-darwin.so`UNICODE_setitem + 296
在multiarray
之上,一直到import pandas
,所有内容都是纯Python。因此,如果您想确切地知道Pandas在调用此函数的位置,您需要在pdb
中进行调试,这是我尚未完成的。但是我认为我们现在已经有足够的信息了。