Python 中将字符串转换为 ctypes.c_ubyte 数组的高效方法

10
我有一个长度为20个字节的字符串,我想将它转换为ctypes.c_ubyte数组以便进行位域操作。
 import ctypes
 str_bytes = '01234567890123456789'
 byte_arr = bytearray(str_bytes)
 raw_bytes = (ctypes.c_ubyte*20)(*(byte_arr))

有没有方法可以避免从str到bytearray的深层复制,以便进行类型转换?或者,是否可以使用像memoryview这样的技术将字符串转换为bytearray而不进行深拷贝?我正在使用Python 2.7。
性能结果:
使用eryksunBrian Larsen的建议,在一个带有Ubuntu 12.04和Python 2.7的vbox VM下进行基准测试。
- method1使用我的原始帖子 - method2使用ctype from_buffer_copy - method3使用ctype cast/POINTER - method4使用numpy
结果:
- method1需要3.87秒 - method2需要0.42秒 - method3需要1.44秒 - method4需要8.79秒
代码:
import ctypes
import time
import numpy

str_bytes = '01234567890123456789'

def method1():
    result = ''
    t0 = time.clock()
    for x in xrange(0,1000000):     
        byte_arr = bytearray(str_bytes)
        result = (ctypes.c_ubyte*20)(*(byte_arr))

    t1 = time.clock()
    print(t1-t0)

    return result

def method2():

    result = ''
    t0 = time.clock()
    for x in xrange(0,1000000):     
        result = (ctypes.c_ubyte * 20).from_buffer_copy(str_bytes)

    t1 = time.clock()
    print(t1-t0)

    return result

def method3():

    result = ''
    t0 = time.clock()
    for x in xrange(0,1000000):     
        result = ctypes.cast(str_bytes, ctypes.POINTER(ctypes.c_ubyte * 20))[0]

    t1 = time.clock()
    print(t1-t0)

    return result

def method4():

    result = ''
    t0 = time.clock()
    for x in xrange(0,1000000):     
        arr = numpy.asarray(str_bytes)
        result = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte*len(str_bytes)))

    t1 = time.clock()
    print(t1-t0)

    return result

print(method1())
print(method2())
print(method3())
print(method4())

对于20个字节,我怀疑没有太多可以优化的。 - user395760
Python字符串是不可变的,所以如果你想修改它们,你只能处理深拷贝的成本。 - sjaensch
不幸的是,我必须每秒执行这个动作几千次。这正成为我的代码的热点。 - askldjd
2个回答

12
我不认为它按你想象的那样工作。 bytearray 创建了字符串的副本。然后解释器将 bytearray 序列拆包到一个 starargs tuple 中,并将其合并到另一个新的 tuple 中,该元组具有其他参数(即使在这种情况下没有参数)。最后,c_ubyte 数组初始化程序循环遍历参数 tuple,以设置 c_ubyte 数组的元素。这是很多工作和复制,只是为了初始化数组。

相反,您可以使用 from_buffer_copy 方法,假设字符串是带有缓冲区接口(而不是Unicode)的字节串:

import ctypes    
str_bytes = '01234567890123456789'
raw_bytes = (ctypes.c_ubyte * 20).from_buffer_copy(str_bytes)

这仍然需要复制字符串,但只需一次,并且效率更高。正如评论中所述,Python字符串是不可变的,可以被interned或用作dict键。即使ctypes让您在实践中违反了它的不可变性,也应该尊重它:

>>> from ctypes import *
>>> s = '01234567890123456789'
>>> b = cast(s, POINTER(c_ubyte * 20))[0]
>>> b[0] = 97
>>> s
'a1234567890123456789'

编辑

我需要强调的是,我不建议使用ctypes修改不可变的CPython字符串。如果你必须这样做,那么至少在修改之前检查sys.getrefcount,以确保引用计数为2或更少(调用会增加1)。否则,你将最终被字符串内部化所困扰(例如"sys"),以及代码对象常量。Python可以自由地根据需要重用不可变对象。如果你跨越语言界限去改变一个“不可变”的对象,那么你就违反了契约。

例如,如果你修改一个已经哈希过的字符串,缓存的哈希值就不再正确匹配其内容。这使得它无法用作字典键。新内容的另一个字符串和原始内容的字符串都无法与字典中的键匹配。前者具有不同的哈希值,后者具有不同的值。然后,唯一的方法是使用具有不正确哈希值的已变异字符串来访问字典项。继续上一个示例:

>>> s
'a1234567890123456789'
>>> d = {s: 1}
>>> d[s]
1

>>> d['a1234567890123456789']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'a1234567890123456789'

>>> d['01234567890123456789']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: '01234567890123456789'

现在考虑一下如果密钥是一个在许多地方重复使用的内部字符串,那么会出现什么混乱。
对于性能分析,通常使用timeit模块。在3.3之前,timeit.default_timer因平台而异。在POSIX系统上,它是time.time,在Windows上,它是time.clock。
import timeit

setup = r'''
import ctypes, numpy
str_bytes = '01234567890123456789'
arr_t = ctypes.c_ubyte * 20
'''

methods = [
  'arr_t(*bytearray(str_bytes))',
  'arr_t.from_buffer_copy(str_bytes)',
  'ctypes.cast(str_bytes, ctypes.POINTER(arr_t))[0]',
  'numpy.asarray(str_bytes).ctypes.data_as('
      'ctypes.POINTER(arr_t))[0]',
]

test = lambda m: min(timeit.repeat(m, setup))

>>> tabs = [test(m) for m in methods]
>>> trel = [t / tabs[0] for t in tabs]
>>> trel
[1.0, 0.060573711879182784, 0.261847116395079, 1.5389279092185282]

我喜欢这两个解决方案。谢谢。 - askldjd
2
那非常有用!谢谢 - BaldDude
我同意,非常详细和有帮助,谢谢! - mara004

1
作为另一种基准测试的解决方案(我对结果非常感兴趣),您可以考虑使用numpy,具体取决于整个代码的情况。
import numpy as np
import ctypes
str_bytes = '01234567890123456789'
arr = np.asarray(str_bytes)
aa = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte*len(str_bytes)))
for v in aa.contents: print v
48
49
50
51
52
53
54
55
56
57
48
49
50
51
52
53
54
55
56
57

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