3

我最近开始学习 Python(我小时候使用 GW BASIC 后的第一门编程语言)。我注意到,当向字节对象添加字节时,每个字节的添加时间都比最后一个要长;相比之下,当向列表对象添加整数时,每个整数与最后一个整数的添加时间相同。下面的程序说明了。

import time
import struct
time.clock() # for Windows

def time_list():
    print("adding 9,999,999 0s to one list 9 times:")
    a = []
    for i in range(9):
        start_time = time.clock()
        for j in range(9999999):
            a += [0]
        end_time = time.clock()
        print("loop %d took %f seconds" %(i, end_time - start_time))
    print()

def time_bytes_object():
    print("adding 99,999 pad bytes to a bytes object 9 times:")
    a = bytes()
    for i in range(9):
        start_time = time.clock()
        for j in range(99999):
            a += struct.pack('<B', 0)
        end_time = time.clock()
        print("loop %d took %f seconds" %(i, end_time - start_time))
    print()

time_list()
time_bytes_object()

是什么字节对象(或 struct.pack 函数)使得添加字节需要越来越多的时间?还是有比我的示例中使用的方法更快的方法来收集一堆字节?

谢谢你的帮助,

胜利者

4

4 回答 4

6

Python 中的字节字符串(和 Unicode 字符串)是不可变的,而列表是可变的。

这意味着+=对字节字符串执行的每个附加()都必须复制该字符串;原件没有被修改(尽管稍后会被垃圾收集)。相比之下,(也被) 使用的append方法实际上会修改列表。list+=

你想要的是bytearray类型,它是一种可变类型,功能很像字节列表。附加到一个bytearray需要(摊销的)常数时间,并且它很容易转换为字节字符串和从字节字符串转换。

于 2012-09-10T20:51:42.587 回答
2

bytes对象和字符串一样是不可变的。每次您这样做时a += something,Python 都会创建一个对象,将其复制a + something到其中,然后将其分配给a.

使用可变序列并支持方法的bytearray类型会更好。append

于 2012-09-10T20:52:39.707 回答
2

我遇到了和你一样的问题,但是我发现这在 python 2.7 中不是问题。

即,在 60mb 文件上运行的相同脚本,一次读取 3 个整数,应用一些更改,然后使用 my_var += in_bytes 将它们附加到“字节”,在 python 3.3 中运行需要 100 倍的时间

python 3.3:172.76 秒

蟒蛇2.7:1.72519993782

于 2014-04-24T08:02:15.233 回答
2

我只想更深入地介绍nneonneos 的答案(我很好奇事情是如何在幕后工作的)。

主要区别已在以下文档中提到bytearray

返回一个新的字节数组。bytearray 类型是0 <= x < 256 范围内的可变整数序列。它具有可变序列的大多数常用方法,在可变序列类型中描述,以及字节类型具有的大多数方法,请参见字节和字节数组方法。

另一方面bytes是不可变的:

返回一个新的“bytes”对象,它是0 <= x < 256 范围内的不可变整数序列。bytes 是 bytearray 的不可变版本——它具有相同的非变异方法以及相同的索引和切片行为。


您可以在 CPython 中轻松演示这一点,其中id()返回对象的地址:

CPython 实现细节:这是内存中对象的地址。

当你扩展bytes()你总是得到新的对象:

>>> a = bytes(b'abc')
>>> id(a)
52845824
>>> a += b'de'
>>> id(a)
52843384

bytearray()仍然是具有不同属性的相同对象

>>> b = bytearray(b'abc')
>>> id(b)
52786352
>>> b += b'de'
>>> id(b)
52786352

我想看看这在内部看起来如何,所以dis派上用场了:

>>> def iadd(a, b):
...     a += b
...     return a
... 
>>> dis.dis(iadd)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 INPLACE_ADD
              7 STORE_FAST               0 (a)

  3          10 LOAD_FAST                0 (a)
             13 RETURN_VALUE

所以我想最后 C 路由PyNumber_InPlaceAdd被调用,所以让我们启动 gdb。


字节

(gdb) b PyNumber_InPlaceAdd
Breakpoint 1 at 0x5317a7: file Objects/abstract.c, line 1066.
(gdb) c
>>> a = b'abc'
>>> a += b'de'

Breakpoint 1, PyNumber_InPlaceAdd (v=0xb030f0, w=0xb03040) at Objects/abstract.c:1066
1066        PyObject *result = binary_iop1(v, w, NB_SLOT(nb_inplace_add),
(gdb) s
binary_iop1 (v=0xb030f0, w=0xb03040, iop_slot=152, op_slot=0) at Objects/abstract.c:1010
1010        PyNumberMethods *mv = v->ob_type->tp_as_number;
(gdb) s
1011        if (mv != NULL) {
(gdb) info locals
mv = 0x0
(gdb) 
1021        return binary_op1(v, w, op_slot);
(gdb) 
binary_op1 (v=0xb030f0, w=0xb03040, op_slot=0) at Objects/abstract.c:765
            ...
(gdb) 
796     return Py_NotImplemented;
PyNumber_InPlaceAdd (v=0xb030f0, w=0xb03040) at Objects/abstract.c:1068
1068        if (result == Py_NotImplemented) {
(gdb) 
1069            PySequenceMethods *m = v->ob_type->tp_as_sequence;
(gdb) 
1070            Py_DECREF(result);
(gdb) info locals
m = 0x856940 <bytes_as_sequence>
(gdb) s
1071            if (m != NULL) {
(gdb) 
1072                binaryfunc f = NULL;
(gdb) 
1073                f = m->sq_inplace_concat;
(gdb) 
1074                if (f == NULL)
(gdb) 
1075                    f = m->sq_concat;
(gdb) 
1076                if (f != NULL)
(gdb) 
1077                    return (*f)(v, w);
(gdb) info locals f
f = 0x54948c <bytes_concat>

所以最后bytes_concat被调用并复制内存:

size = va.len + vb.len;
if (size < 0) {
    PyErr_NoMemory();
    goto done;
}

result = PyBytes_FromStringAndSize(NULL, size);
if (result != NULL) {
    memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
    memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
}

字节数组

它有类似的流程(也以 结束PyNumber_InPlaceAdd):

>>> b = bytearray(b'abc')
[57107 refs]
>>> b += b'de'

Breakpoint 1, PyNumber_InPlaceAdd (v=0xa35b80, w=0xb03040) at Objects/abstract.c:1066
1066        PyObject *result = binary_iop1(v, w, NB_SLOT(nb_inplace_add),
(gdb) n
1068        if (result == Py_NotImplemented) {
(gdb) s
1069            PySequenceMethods *m = v->ob_type->tp_as_sequence;
(gdb) 
1070            Py_DECREF(result);
(gdb) 
1071            if (m != NULL) {
(gdb) 
1072                binaryfunc f = NULL;
(gdb) 
1073                f = m->sq_inplace_concat;
(gdb) 
1074                if (f == NULL)
(gdb) info locals
f = 0x537901 <bytearray_iconcat>
(gdb) n
1076                if (f != NULL)
(gdb) 
1077                    return (*f)(v, w);

bytes指针m->sq_inplace_concat为空,因此被m->sq_concat调用,bytearray已设置,因此bytearray_iconcat被调用并以:

if (size < self->ob_alloc) {
    Py_SIZE(self) = size;
    PyByteArray_AS_STRING(self)[Py_SIZE(self)] = '\0'; /* Trailing null byte */
}
else if (PyByteArray_Resize((PyObject *)self, size) < 0) {
    PyBuffer_Release(&vo);
    return NULL;
}
memcpy(PyByteArray_AS_STRING(self) + mysize, vo.buf, vo.len);

因此,速度差异(在我的情况下,我需要bytes从 80000+ 块中构建一个,bytes并且需要 180 秒,bytearray0.260)。

于 2015-05-01T06:46:35.730 回答