Чому ітерація над невеликим рядком повільніше, ніж за малим списком?


132

Я розігрувався з timeit і зауважив, що робити просте розуміння списку невеликою струною потрібно довше, ніж виконувати ту саму операцію зі списком невеликих одиночних рядків символів. Будь-яке пояснення? Це майже в 1,35 рази більше часу.

>>> from timeit import timeit
>>> timeit("[x for x in 'abc']")
2.0691067844831528
>>> timeit("[x for x in ['a', 'b', 'c']]")
1.5286479570345861

Що відбувається на нижчому рівні, що викликає це?

Відповіді:


193

TL; DR

  • Фактична різниця швидкостей ближче до 70% (або більше), коли багато знімається накладних витрат для Python 2.

  • Створення об'єкта не винне. Жоден метод не створює новий об'єкт, оскільки односимвольні рядки кешуються.

  • Різниця не очевидна, але, ймовірно, створюється за рахунок більшої кількості перевірок строкової індексації, що стосується типу та добре сформованої форми. Це також цілком ймовірно завдяки необхідності перевірити, що повернути.

  • Індексація списку надзвичайно швидко.



>>> python3 -m timeit '[x for x in "abc"]'
1000000 loops, best of 3: 0.388 usec per loop

>>> python3 -m timeit '[x for x in ["a", "b", "c"]]'
1000000 loops, best of 3: 0.436 usec per loop

Це не погоджується з тим, що ви знайшли ...

Ви, мабуть, використовуєте Python 2.

>>> python2 -m timeit '[x for x in "abc"]'
1000000 loops, best of 3: 0.309 usec per loop

>>> python2 -m timeit '[x for x in ["a", "b", "c"]]'
1000000 loops, best of 3: 0.212 usec per loop

Пояснимо різницю між версіями. Я вивчу складений код.

Для Python 3:

import dis

def list_iterate():
    [item for item in ["a", "b", "c"]]

dis.dis(list_iterate)
#>>>   4           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f4d06b118a0, file "", line 4>)
#>>>               3 LOAD_CONST               2 ('list_iterate.<locals>.<listcomp>')
#>>>               6 MAKE_FUNCTION            0
#>>>               9 LOAD_CONST               3 ('a')
#>>>              12 LOAD_CONST               4 ('b')
#>>>              15 LOAD_CONST               5 ('c')
#>>>              18 BUILD_LIST               3
#>>>              21 GET_ITER
#>>>              22 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
#>>>              25 POP_TOP
#>>>              26 LOAD_CONST               0 (None)
#>>>              29 RETURN_VALUE

def string_iterate():
    [item for item in "abc"]

dis.dis(string_iterate)
#>>>  21           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f4d06b17150, file "", line 21>)
#>>>               3 LOAD_CONST               2 ('string_iterate.<locals>.<listcomp>')
#>>>               6 MAKE_FUNCTION            0
#>>>               9 LOAD_CONST               3 ('abc')
#>>>              12 GET_ITER
#>>>              13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
#>>>              16 POP_TOP
#>>>              17 LOAD_CONST               0 (None)
#>>>              20 RETURN_VALUE

Ви бачите тут, що варіант списку, ймовірно, буде повільнішим через побудову списку кожного разу.

Це

 9 LOAD_CONST   3 ('a')
12 LOAD_CONST   4 ('b')
15 LOAD_CONST   5 ('c')
18 BUILD_LIST   3

частина. Варіант рядка є лише

 9 LOAD_CONST   3 ('abc')

Ви можете перевірити, чи це, здається, має значення:

def string_iterate():
    [item for item in ("a", "b", "c")]

dis.dis(string_iterate)
#>>>  35           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f4d068be660, file "", line 35>)
#>>>               3 LOAD_CONST               2 ('string_iterate.<locals>.<listcomp>')
#>>>               6 MAKE_FUNCTION            0
#>>>               9 LOAD_CONST               6 (('a', 'b', 'c'))
#>>>              12 GET_ITER
#>>>              13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
#>>>              16 POP_TOP
#>>>              17 LOAD_CONST               0 (None)
#>>>              20 RETURN_VALUE

Це виробляє просто

 9 LOAD_CONST               6 (('a', 'b', 'c'))

так як кортежі незмінні. Тест:

>>> python3 -m timeit '[x for x in ("a", "b", "c")]'
1000000 loops, best of 3: 0.369 usec per loop

Чудово, підтримуйте швидкість.

Для Python 2:

def list_iterate():
    [item for item in ["a", "b", "c"]]

dis.dis(list_iterate)
#>>>   2           0 BUILD_LIST               0
#>>>               3 LOAD_CONST               1 ('a')
#>>>               6 LOAD_CONST               2 ('b')
#>>>               9 LOAD_CONST               3 ('c')
#>>>              12 BUILD_LIST               3
#>>>              15 GET_ITER            
#>>>         >>   16 FOR_ITER                12 (to 31)
#>>>              19 STORE_FAST               0 (item)
#>>>              22 LOAD_FAST                0 (item)
#>>>              25 LIST_APPEND              2
#>>>              28 JUMP_ABSOLUTE           16
#>>>         >>   31 POP_TOP             
#>>>              32 LOAD_CONST               0 (None)
#>>>              35 RETURN_VALUE        

def string_iterate():
    [item for item in "abc"]

dis.dis(string_iterate)
#>>>   2           0 BUILD_LIST               0
#>>>               3 LOAD_CONST               1 ('abc')
#>>>               6 GET_ITER            
#>>>         >>    7 FOR_ITER                12 (to 22)
#>>>              10 STORE_FAST               0 (item)
#>>>              13 LOAD_FAST                0 (item)
#>>>              16 LIST_APPEND              2
#>>>              19 JUMP_ABSOLUTE            7
#>>>         >>   22 POP_TOP             
#>>>              23 LOAD_CONST               0 (None)
#>>>              26 RETURN_VALUE        

Дивно, що у нас однаковий склад списку, але це все ж швидше для цього. Python 2 діє надзвичайно швидко.

Давайте знімемо розуміння та повторимо час. Це _ =полягає у тому, щоб запобігти його оптимізації.

>>> python3 -m timeit '_ = ["a", "b", "c"]'
10000000 loops, best of 3: 0.0707 usec per loop

>>> python3 -m timeit '_ = "abc"'
100000000 loops, best of 3: 0.0171 usec per loop

Ми можемо бачити, що ініціалізація недостатньо істотна для врахування різниці між версіями (ці числа невеликі)! Таким чином, ми можемо зробити висновок, що Python 3 має більш повільне розуміння. Це має сенс, оскільки Python 3 змінив уявлення, щоб забезпечити більш безпечне застосування.

Ну, а тепер покращимо еталонний показник (я просто знімаю накладні витрати, це не ітерація). Це видаляє будинок, який можна повторити, попередньо призначивши його:

>>> python3 -m timeit -s 'iterable = "abc"'           '[x for x in iterable]'
1000000 loops, best of 3: 0.387 usec per loop

>>> python3 -m timeit -s 'iterable = ["a", "b", "c"]' '[x for x in iterable]'
1000000 loops, best of 3: 0.368 usec per loop
>>> python2 -m timeit -s 'iterable = "abc"'           '[x for x in iterable]'
1000000 loops, best of 3: 0.309 usec per loop

>>> python2 -m timeit -s 'iterable = ["a", "b", "c"]' '[x for x in iterable]'
10000000 loops, best of 3: 0.164 usec per loop

Ми можемо перевірити, чи iterє дзвінок накладним:

>>> python3 -m timeit -s 'iterable = "abc"'           'iter(iterable)'
10000000 loops, best of 3: 0.099 usec per loop

>>> python3 -m timeit -s 'iterable = ["a", "b", "c"]' 'iter(iterable)'
10000000 loops, best of 3: 0.1 usec per loop
>>> python2 -m timeit -s 'iterable = "abc"'           'iter(iterable)'
10000000 loops, best of 3: 0.0913 usec per loop

>>> python2 -m timeit -s 'iterable = ["a", "b", "c"]' 'iter(iterable)'
10000000 loops, best of 3: 0.0854 usec per loop

Ні. Ні, це не так. Різниця занадто мала, особливо для Python 3.

Тож давайте видалимо ще небажані накладні витрати, зробивши все це повільніше! Мета полягає лише в тому, щоб мати більш тривалу ітерацію, щоб час ховався над головою.

>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' '[x for x in iterable]'
100 loops, best of 3: 3.12 msec per loop

>>> python3 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' '[x for x in iterable]'
100 loops, best of 3: 2.77 msec per loop
>>> python2 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' '[x for x in iterable]'
100 loops, best of 3: 2.32 msec per loop

>>> python2 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' '[x for x in iterable]'
100 loops, best of 3: 2.09 msec per loop

Це насправді мало що змінилося , але це мало допомогло.

Тож зніміть розуміння. Це не є головним питанням:

>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'for x in iterable: pass'
1000 loops, best of 3: 1.71 msec per loop

>>> python3 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'for x in iterable: pass'
1000 loops, best of 3: 1.36 msec per loop
>>> python2 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'for x in iterable: pass'
1000 loops, best of 3: 1.27 msec per loop

>>> python2 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'for x in iterable: pass'
1000 loops, best of 3: 935 usec per loop

Більше схоже на це! Ми все ще можемо отримати трохи швидше, використовуючи dequeітерацію. Це в основному те саме, але це швидше :

>>> python3 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 777 usec per loop

>>> python3 -m timeit -s 'import random; from collections import deque; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 405 usec per loop
>>> python2 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 805 usec per loop

>>> python2 -m timeit -s 'import random; from collections import deque; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 438 usec per loop

Що мене вражає, це те, що Unicode є конкурентоспроможним за допомогою біотествілл. Ми можемо перевірити це чітко, спробувавши bytesі unicodeв обох:

  • bytes

    >>> python3 -m timeit -s 'import random; from collections import deque; iterable = b"".join(chr(random.randint(0, 127)).encode("ascii") for _ in range(100000))' 'deque(iterable, maxlen=0)'                                                                    :(
    1000 loops, best of 3: 571 usec per loop
    
    >>> python3 -m timeit -s 'import random; from collections import deque; iterable =         [chr(random.randint(0, 127)).encode("ascii") for _ in range(100000)]' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 394 usec per loop
    >>> python2 -m timeit -s 'import random; from collections import deque; iterable = b"".join(chr(random.randint(0, 127))                 for _ in range(100000))' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 757 usec per loop
    
    >>> python2 -m timeit -s 'import random; from collections import deque; iterable =         [chr(random.randint(0, 127))                 for _ in range(100000)]' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 438 usec per loop

    Тут ви бачите Python 3 насправді швидше, ніж Python 2.

  • unicode

    >>> python3 -m timeit -s 'import random; from collections import deque; iterable = u"".join(   chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 800 usec per loop
    
    >>> python3 -m timeit -s 'import random; from collections import deque; iterable =         [   chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 394 usec per loop
    >>> python2 -m timeit -s 'import random; from collections import deque; iterable = u"".join(unichr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 1.07 msec per loop
    
    >>> python2 -m timeit -s 'import random; from collections import deque; iterable =         [unichr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
    1000 loops, best of 3: 469 usec per loop

    Знову ж таки, Python 3 швидший, хоча цього можна очікувати ( strприділяв багато уваги в Python 3).

Насправді це unicode- bytesрізниця дуже мала, що вражає.

Тож давайте проаналізуємо цей один випадок, побачивши, як це швидко і зручно для мене:

>>> python3 -m timeit -s 'import random; from collections import deque; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 777 usec per loop

>>> python3 -m timeit -s 'import random; from collections import deque; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'deque(iterable, maxlen=0)'
1000 loops, best of 3: 405 usec per loop

Насправді ми можемо виключити відповідь Тіма Петра у 10 разів більш схваленій відповіді!

>>> foo = iterable[123]
>>> iterable[36] is foo
True

Це не нові об’єкти!

Але це варто зазначити: індексація витрат . Різниця, ймовірно, буде в індексації, тому видаліть ітерацію та просто індекс:

>>> python3 -m timeit -s 'import random; iterable = "".join(chr(random.randint(0, 127)) for _ in range(100000))' 'iterable[123]'
10000000 loops, best of 3: 0.0397 usec per loop

>>> python3 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'iterable[123]'
10000000 loops, best of 3: 0.0374 usec per loop

Різниця здається невеликою, але принаймні половина витрат є накладними:

>>> python3 -m timeit -s 'import random; iterable =        [chr(random.randint(0, 127)) for _ in range(100000)]' 'iterable; 123'
100000000 loops, best of 3: 0.0173 usec per loop

тому різниці швидкостей достатньо, щоб вирішити звинувачувати її. Я думаю.

То чому індексація списку настільки швидша?

Що ж, я повернуся до вас з цього приводу, але я гадаю, що це все-таки до перевірки інтернованих рядків (або кешованих символів, якщо це окремий механізм). Це буде менш швидко, ніж оптимально. Але я піду перевірити джерело (хоча мені не комфортно в С ...) :).


Тож ось джерело:

static PyObject *
unicode_getitem(PyObject *self, Py_ssize_t index)
{
    void *data;
    enum PyUnicode_Kind kind;
    Py_UCS4 ch;
    PyObject *res;

    if (!PyUnicode_Check(self) || PyUnicode_READY(self) == -1) {
        PyErr_BadArgument();
        return NULL;
    }
    if (index < 0 || index >= PyUnicode_GET_LENGTH(self)) {
        PyErr_SetString(PyExc_IndexError, "string index out of range");
        return NULL;
    }
    kind = PyUnicode_KIND(self);
    data = PyUnicode_DATA(self);
    ch = PyUnicode_READ(kind, data, index);
    if (ch < 256)
        return get_latin1_char(ch);

    res = PyUnicode_New(1, ch);
    if (res == NULL)
        return NULL;
    kind = PyUnicode_KIND(res);
    data = PyUnicode_DATA(res);
    PyUnicode_WRITE(kind, data, 0, ch);
    assert(_PyUnicode_CheckConsistency(res, 1));
    return res;
}

Ідучи зверху, ми перевіримо. Це нудно. Потім деякі призначення, які також повинні бути нудними. Перша цікава лінія

ch = PyUnicode_READ(kind, data, index);

але ми сподіваємось, що це швидко, оскільки ми читаємо з суміжного масиву C, індексуючи його. Результат, chбуде менше 256, тому ми повернемо кешований символ у get_latin1_char(ch).

Тож ми побіжимо (скинувши перші чеки)

kind = PyUnicode_KIND(self);
data = PyUnicode_DATA(self);
ch = PyUnicode_READ(kind, data, index);
return get_latin1_char(ch);

Де

#define PyUnicode_KIND(op) \
    (assert(PyUnicode_Check(op)), \
     assert(PyUnicode_IS_READY(op)),            \
     ((PyASCIIObject *)(op))->state.kind)

(що нудно, тому що твердження ігноруються при налагодженні [тому я можу перевірити, що вони швидкі] і ((PyASCIIObject *)(op))->state.kind)є (я думаю) непрямим та на рівні С);

#define PyUnicode_DATA(op) \
    (assert(PyUnicode_Check(op)), \
     PyUnicode_IS_COMPACT(op) ? _PyUnicode_COMPACT_DATA(op) :   \
     _PyUnicode_NONCOMPACT_DATA(op))

(що також нудно з аналогічних причин, якщо припустити, що макроси ( Something_CAPITALIZED) всі швидкі),

#define PyUnicode_READ(kind, data, index) \
    ((Py_UCS4) \
    ((kind) == PyUnicode_1BYTE_KIND ? \
        ((const Py_UCS1 *)(data))[(index)] : \
        ((kind) == PyUnicode_2BYTE_KIND ? \
            ((const Py_UCS2 *)(data))[(index)] : \
            ((const Py_UCS4 *)(data))[(index)] \
        ) \
    ))

(що включає в себе індекси, але насправді зовсім не повільно) і

static PyObject*
get_latin1_char(unsigned char ch)
{
    PyObject *unicode = unicode_latin1[ch];
    if (!unicode) {
        unicode = PyUnicode_New(1, ch);
        if (!unicode)
            return NULL;
        PyUnicode_1BYTE_DATA(unicode)[0] = ch;
        assert(_PyUnicode_CheckConsistency(unicode, 1));
        unicode_latin1[ch] = unicode;
    }
    Py_INCREF(unicode);
    return unicode;
}

Що підтверджує мою підозру, що:

  • Це кешовано:

    PyObject *unicode = unicode_latin1[ch];
  • Це повинно бути швидким. Це if (!unicode)не запускається, тому в цьому випадку це буквально еквівалентно

    PyObject *unicode = unicode_latin1[ch];
    Py_INCREF(unicode);
    return unicode;

Чесно кажучи, після тестування asserts швидко проходять (відключивши їх (я думаю, це працює на C-рівні, стверджує ...)), єдино правдоподібно-повільні частини:

PyUnicode_IS_COMPACT(op)
_PyUnicode_COMPACT_DATA(op)
_PyUnicode_NONCOMPACT_DATA(op)

Які є:

#define PyUnicode_IS_COMPACT(op) \
    (((PyASCIIObject*)(op))->state.compact)

(швидко, як і раніше),

#define _PyUnicode_COMPACT_DATA(op)                     \
    (PyUnicode_IS_ASCII(op) ?                   \
     ((void*)((PyASCIIObject*)(op) + 1)) :              \
     ((void*)((PyCompactUnicodeObject*)(op) + 1)))

(швидко, якщо макрос IS_ASCIIшвидкий), і

#define _PyUnicode_NONCOMPACT_DATA(op)                  \
    (assert(((PyUnicodeObject*)(op))->data.any),        \
     ((((PyUnicodeObject *)(op))->data.any)))

(також швидко, оскільки це утвердження плюс непрямість плюс акторський склад).

Таким чином, ми вниз (кроляча нора) до:

PyUnicode_IS_ASCII

який є

#define PyUnicode_IS_ASCII(op)                   \
    (assert(PyUnicode_Check(op)),                \
     assert(PyUnicode_IS_READY(op)),             \
     ((PyASCIIObject*)op)->state.ascii)

Гм ... це теж здається швидко ...


Ну добре, але давайте порівняємо це PyList_GetItem. (Так, дякую Тіму Пітерсу за те, що мені дали більше роботи: P.)

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    if (!PyList_Check(op)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    if (i < 0 || i >= Py_SIZE(op)) {
        if (indexerr == NULL) {
            indexerr = PyUnicode_FromString(
                "list index out of range");
            if (indexerr == NULL)
                return NULL;
        }
        PyErr_SetObject(PyExc_IndexError, indexerr);
        return NULL;
    }
    return ((PyListObject *)op) -> ob_item[i];
}

Ми можемо побачити, що у випадках без помилок це буде просто запущено:

PyList_Check(op)
Py_SIZE(op)
((PyListObject *)op) -> ob_item[i]

Де PyList_Checkє

#define PyList_Check(op) \
     PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)

( TABS! TABS !!! ) ( issue21587 ) Це зафіксувались і об’єдналися за 5 хвилин . Як ... так. Блін. Вони соромлять Скіта.

#define Py_SIZE(ob)             (((PyVarObject*)(ob))->ob_size)
#define PyType_FastSubclass(t,f)  PyType_HasFeature(t,f)
#ifdef Py_LIMITED_API
#define PyType_HasFeature(t,f)  ((PyType_GetFlags(t) & (f)) != 0)
#else
#define PyType_HasFeature(t,f)  (((t)->tp_flags & (f)) != 0)
#endif

Отже, це звичайно справді тривіально (дві непрямі та кілька булевих перевірок), якщо Py_LIMITED_APIнемає, у такому випадку ... ???

Потім є індексація та каст ( ((PyListObject *)op) -> ob_item[i]) і ми закінчили.

Тож на перелік списків безумовно менше , і невеликі перепади швидкостей, безумовно, означають, що це може бути доречним.


Я думаю, що в цілому (->)для Unicode є просто більше перевірки типу та непрямості. Здається, я пропускаю крапку, але що ?


17
Ви представляєте код як пояснювальний; ви навіть представляєте фрагменти як висновки. На жаль для мене, я не можу наслідувати цього. Не кажучи про те, що ваш підхід до з'ясування того, що не так, не є твердим, але було б добре, якби було легше наслідувати.
PascalVKooten

2
Я намагався його вдосконалити, але не знаю, як зробити це більш зрозумілим. Зауважте, що я не пишу С, тому це аналіз коду на високому рівні і важливі лише загальні поняття.
Ведрак

@Nit я додав. Скажіть, якщо відчуває нестачу. На жаль, це також підкреслює, що я фактично не знаю відповіді (* задихатися *).
Ведрак

3
Я дам це ще день, перш ніж прийму вашу відповідь (я хотів би побачити щось конкретніше, що з'явиться), але дякую за дуже цікаву і добре вивчену відповідь.
Sunjay Varma

4
Зауважте, що ви знімаєте рухому ціль ;-) Ця реалізація відрізняється не лише між Python 2 та Python 3, але й між різними версіями. Наприклад, на поточному магістралі розвитку get_latin1_char()фокус вже існує не unicode_getitem()в нижчому рівні unicode_char. Отже, зараз є ще один рівень виклику функції - чи ні (залежно від використовуваних прапорців компілятора та оптимізації). На цей рівень деталізації просто немає достовірних відповідей ;-)
Тім Пітерс,

31

Коли ви перебираєте більшість об'єктів контейнерів (списки, кортежі, дикти, ...), ітератор доставляє об'єкти в контейнер.

Але коли ви перебираєте рядок, для кожного доставленого символу повинен бути створений новий об'єкт - рядок не є "контейнером", у тому ж сенсі список є контейнером. Окремі символи в рядку не існують як окремі об'єкти до того, як ітерація створює ці об'єкти.


3
Я не думаю, що це насправді. Ви можете перевірити is. Це звучить правильно, але я дійсно не думаю, що це може бути.
Ведрак

Погляньте на відповідь @Veedrac.
Крістіан

3
stringobject.cпоказує, що __getitem__для рядків просто отримує результат з таблиці збережених 1-символьних рядків, тому витрати на розподіл для них виникають лише один раз.
user2357112 підтримує Моніку

10
@ user2357112, так, для простих рядків в Python 2 це життєво важливий момент. У Python 3 всі рядки є "офіційно" Unicode, і багато деталей задіяно (див. Відповідь Ведрака). Наприклад, в Python 3, після того s = chr(256), s is chr(256)повертається False- знаючи тип мало, тому що кургани особливих випадків існують під кришками запускають на даних значень .
Тім Пітерс

1

Можна створити і накладні витрати на створення ітератора для рядка. Тоді як масив вже містить ітератор при створенні даних.

Редагувати:

>>> timeit("[x for x in ['a','b','c']]")
0.3818681240081787
>>> timeit("[x for x in 'abc']")
0.3732869625091553

Це було проведено за допомогою 2.7, але на моїй mac book pro i7. Це може бути результатом різниці в конфігурації системи.


Навіть лише використовуючи прямі ітератори, рядок все ще значно повільніше. timeit ("[x for x in it]", "it = iter ('abc')") = 0,34543599384033535; timeit ("[x for x in it]", "it = iter (list ('abc'))") = 0.2791691380446508
Sunjay Varma
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.