当您import pandas 时,它会执行大量 NumPy 操作,包括在所有单 ASCII 字母字符串上调用 UNICODE_setitem,并且可能在其他地方对单 ASCII 数字字符串执行类似操作。
该 NumPy 函数调用已弃用的 C API PyUnicode_AsUnicode。
当您在 CPython 3.3+ 中调用它时,它会将 wchar_t * 表示缓存在字符串的内部结构中,在其 wstr 成员中,作为两个 wchar_t 值 w'a' 和 '\0',占用 8 个字节Python 的 32 位wchar_t 构建。 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 安装上(但在 Linux 或 macOS 上都是 64 位 CPython 3.4 或更高版本),我得到了相同的结果:
{'\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 数字和字母,但不会影响其他任何东西。
此外,如果您将所有 prints 更改为 print(sizes.values()),那么字符串永远不会被编码以输出,您会得到相同的结果,这意味着它不是关于缓存 UTF-8,或者它是的,但即使我们不强制,这种情况总是会发生。
显而易见的可能性是,无论 Pandas 调用什么,都使用 legacy PyUnicode API 之一为所有 ASCII 数字和字母生成单字符串。所以这些字符串最终不是紧凑的 ASCII 格式,而是遗留的格式,对吧? (有关这意味着什么的详细信息,请参阅the comments in the source。)
不。使用我的 superhackyinternals 中的代码,我们可以看到它仍然是 compact-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-compact 字符串?
简单:您在其上调用 PyUnicode_AsUnicode(或访问 3.2 样式本机 wchar_t * 内部存储的其他已弃用函数或宏之一。该本机内部存储实际上并不存在于 3.3+ 中。所以,为了向后兼容,这些调用是通过动态创建该存储,将其粘贴在wstr 成员上,并调用适当的PyUnicode_AsUCS[24] 函数来解码到该存储来处理的。(除非您正在处理一个紧凑的字符串,其kind 恰好匹配 wchar_t 的宽度,在这种情况下,wstr 毕竟只是一个指向本机存储的指针。)
您希望str.__sizeof__ 理想地包含该额外存储,而from the source,您可以看到它确实如此。
让我们验证一下:
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 中调试,我还没有这样做。但我认为我们现在已经掌握了足够的信息。