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

时间:2014-01-31 15:17:52

标签: python bytearray ctypes

我有一个20字节的字符串,我想将它转换为ctypes.c_ubyte数组以进行位字段操作。

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

有没有办法避免为了演员而从str到bytearray的深拷贝?

或者,是否可以在没有深层复制的情况下将字符串转换为bytearray? (使用memoryview等技术?)

我正在使用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())

2 个答案:

答案 0 :(得分:8)

我不这样做你的想法。 bytearray创建字符串的副本。然后解释器将bytearray序列解压缩到starargs tuple并将其合并到另一个具有其他args的新tuple中(即使在这种情况下没有)。最后,c_ubyte数组初始化程序遍历args tuple以设置c_ubyte数组的元素。这需要大量的工作和大量的复制才能初始化阵列。

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

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

仍然需要复制字符串,但它只执行一次,效率更高。正如评论中所述,Python字符串是不可变的,可以实现或用作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可以自由地重用不可变对象。如果你走出语言来改变一个“不可变”的对象,你就违反了合同。

例如,如果修改已经散列的字符串,则缓存的散列不再适用于内容。这打破了它作为dict键使用。具有新内容的另一个字符串或具有原始内容的字符串都不会与字典中的键匹配。前者具有不同的哈希值,后者具有不同的值。然后,获取dict项的唯一方法是使用具有错误哈希的变异字符串。继续前一个例子:

>>> 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]

答案 1 :(得分: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