好吧,听我说,这可不像你想的那么愚蠢。
首先,一些背景知识:我最近开始使用ctypes模块,作为一个技术测试,我想编写一个Mandelbrot浏览器,分别使用pygame和ctypes进行事件处理和访问Mandelbrot计算dll。我最初的计划是通过让Mandelbrot函数计算并存储字符数组中整行像素的值并返回指向该数组的指针来最小化ctypes包装开销:
Mandelbrot.restype = c_char_p
#...
str_location = Mandelbrot(x)
row = str_location.value
但事实证明这并不是真的管用。value方法有两个缺陷:它会降低性能,因为它将C字符串逐字节复制到python字符串中,并且它不知道字符串的预期长度,因此数据中的任何零都将被视为空终止符,从而导致任何进一步的数据丢失。
我的第一个动作是创建一个快速的DLL,允许我反汇编一些Python对象。它具有以下两个功能:
#define DLLINFO extern "C" __declspec(dllexport)
DLLINFO char show_char(char *p)
{
return *p;
}
DLLINFO void mov(char *p, char payload)
{
*p = payload;
}
我还将show_char函数打包为一个Python函数show_object,它使用sys.getsizeof打印Python对象的内存内容。
分解字符串显示了一个非常简单的设计:
>>> from hack import *; import sys
>>>
>>> #string experiment
>>> a = '01234567'
>>> hex(sys.getrefcount(a))
'0x3'
>>> hex(id(type(a)))
'0x1e1d81f8'
>>> hex(len(a))
'0x8'
>>> show_object(a)
3 2 1 0 byte
0 0 0 4 0 #reference count (+1 temporary reference)
1e 1d 81 f8 4 #pointer to type
0 0 0 8 8 #length
94 b b6 98 12 #???
0 0 0 1 16 #???
33 32 31 30 20 #Data '0123' (little endian)
37 36 35 34 24 #Data '4567'
0 28 #Null terminator
>>> #sys.getsizeof reported 29 bytes for 9 bytes of data.
(之后添加数据注释)
我试着用可变bytearray替换字符串,然后我反汇编了一个bytearray,看看我应该将Mandelbrot数据写到哪里:
>>> #bytearray experiment
>>> b = bytearray('01234567')
>>> hex(sys.getrefcount(b))
'0x2'
>>> hex(id(type(b)))
'0x1e1e5e20'
>>> hex(len(b))
'0x8'
>>> show_object(b)
3 2 1 0 byte
0 0 0 3 0 #reference count (+1 temporary reference)
1e 1e 5e 20 4 #pointer to type
0 0 0 8 8 #length
0 0 0 0 12 #???
0 0 0 9 16 #???
2 3a 63 a0 20 #???
2 92 93 38 24 #???
2 91 e4 90 28 #???
1 32 #???
>>> #sys.getsizeof reported 33 bytes for 8 bytes of data
好吧,我搞不清数据是从哪里来的,所以没有骰子。
我的下一个计划是用ctypes内置的可变字符串替换字符串,即create_string_buffer。
>>> #buffer experiment
>>> from ctypes import *
>>> c = create_string_buffer('01234567')
>>> hex(id(type(c)))
'0x1ceb778'
>>> show_object(c)
3 2 1 0 byte
0 0 0 3 0 #reference count
1 ce b7 78 4 #pointer to type
2 38 f7 38 8 #???
0 0 0 1 12 #Here be dragons
0 0 0 0 16 #etc.
0 0 0 9 20
0 0 0 9 24
0 0 0 0 28
0 0 0 0 32
0 0 0 0 36
33 32 31 30 40 #data '0123'
37 36 35 34 44 #data '4567'
0 0 0 0 48
0 0 0 0 52
0 0 0 0 56
0 0 0 0 60
2 38 f8 40 64
2 38 f7 a0 68
ff ff ff fe 72
0 2e 0 65 76
>>> #sys.getsizeof reported 80 bytes for 9 bytes of data.
嗯。至少数据在里面的某个地方。不幸的是,这个对象太冗长,不实用。而且,它不是内置类型,所以我很难让它与其他函数一起工作。
这时我决定切换回字符串并运行一些谨慎的测试来修改字符串:
>>> from hack import *
>>> s = "Hello, world!"
>>> show_object(s)
3 2 1 0 byte
0 0 0 3 0
1e 1d 81 f8 4
0 0 0 d 8
8f 8d ce 9c 12
0 0 0 0 16
6c 6c 65 48 20
77 20 2c 6f 24
64 6c 72 6f 28
0 21 32
>>> mov(id(s)+32, 63)
>>> print s
Hello, world?
>>> mov(id(s)+8,5)
>>> print s
Hello
到现在为止,一直都还不错。至少在我做这几次的时候没有什么崩溃。事实上,即使将长度修改为较低的值也不会立即引起任何问题。(不过,我不打算这么做)
那么,为什么我要问这个问题后,布局数据显示字符串是可变的?
首先,我知道硬件可能会将字符串标记为不可变,并且尝试修改它们可能会导致segfault或类似问题:
char good_string[80];
good_string[8] = '!'; //Everything's okay so far.
char* bad_string = "This string's made out of const chars, beware!";
bad_string[8] = '!'; //And now you've got segfault!
第二点也是更重要的一点,我对Python的内部工作机制了解得还不够,无法自信地绕过Python对字符串的锁定,玩弄未定义的行为。现在,很容易让我相信Python FAQ中声明的字符串不变性的原因是错误的(我没有改变字符串的大小,字符串不像整数那样是基本的),但是我不知道是否有一些隐藏的原因字符串不应该被修改,如果我试着做什么,会有什么东西在我面前爆炸我计划这么做。这是我提出这个问题的主要原因;我希望有更多知识的人能启发我。
谢谢,你读了整个问题。对不起,简洁不是我的强项。:)
最佳答案
有些计算机系统在硬件级别上可以将任意范围的内存标记为只读,但这不是python中的情况。根据定义,python防止在创建字符串时更改字符串。
是的-通过更改python代码或提供一个新的内置代码,完全有可能编写在某些情况下允许字符串可变的代码,但是如果您尝试将可变字符串用作字典键(例如,并明确给定字符串的存储方式),则会遇到真正的困难,更改长度是很困难的(如果在大多数情况下不是不可能的话——例如,您需要在当前字符串之后立即释放内存以扩展到)。
请记住,即使语言有直接内存访问(例如C)这个术语,它的字符串也只能在某些情况下是可变的:您可以更改特定的字符,但如果不预先为它保留内存,就不能任意扩展C字符串的长度,或者在每次更改时更改它的标识(如果有多个引用,则会出现问题)。