¿Por qué es más lento iterar sobre una cadena pequeña que una lista pequeña?

132

Estaba jugando con timeit y noté que hacer una simple comprensión de la lista a través de una cadena pequeña tomó más tiempo que hacer la misma operación en una lista de cadenas de caracteres pequeños. Alguna explicación? Es casi 1,35 veces más tiempo.

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

¿Qué está sucediendo en un nivel inferior que está causando esto?

Sunjay Varma
fuente

Respuestas:

193

TL; DR

  • La diferencia de velocidad real está más cerca del 70% (o más) una vez que se elimina gran parte de la sobrecarga, para Python 2.

  • La creación de objetos no tiene la culpa. Ninguno de los métodos crea un nuevo objeto, ya que las cadenas de un carácter se almacenan en caché.

  • La diferencia no es obvia, pero probablemente se crea a partir de un mayor número de comprobaciones en la indexación de cadenas, con respecto al tipo y la buena formabilidad. También es bastante probable gracias a la necesidad de verificar qué devolver.

  • La indexación de listas es notablemente rápida.



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

Esto no está de acuerdo con lo que has encontrado ...

Debes estar usando Python 2, entonces.

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

Vamos a explicar la diferencia entre las versiones. Examinaré el código compilado.

Para 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

Aquí puede ver que la variante de la lista probablemente sea más lenta debido a la construcción de la lista cada vez.

Este es el

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

parte. La variante de cadena solo tiene

 9 LOAD_CONST   3 ('abc')

Puede comprobar que esto parece marcar la diferencia:

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

Esto produce solo

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

Como las tuplas son inmutables. Prueba:

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

Genial, vuelve a la velocidad.

Para 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        

Lo extraño es que tenemos la misma construcción de la lista, pero aún así es más rápido para esto. Python 2 está actuando extrañamente rápido.

Eliminemos las comprensiones y el tiempo. El _ =objetivo es evitar que se optimice.

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

¡Podemos ver que la inicialización no es lo suficientemente significativa como para explicar la diferencia entre las versiones (esos números son pequeños)! Por lo tanto, podemos concluir que Python 3 tiene comprensiones más lentas. Esto tiene sentido ya que Python 3 cambió las comprensiones para tener un alcance más seguro.

Bueno, ahora mejora el punto de referencia (solo estoy eliminando la sobrecarga que no es iteración). Esto elimina la construcción del iterable al preasignarlo:

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

Podemos verificar si llamar iteres la sobrecarga:

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

No. No, no lo es. La diferencia es demasiado pequeña, especialmente para Python 3.

Así que eliminemos aún más gastos indirectos no deseados ... ¡haciendo todo más lento! El objetivo es simplemente tener una iteración más larga para que el tiempo se oculte por encima.

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

En realidad, esto no ha cambiado mucho , pero ha ayudado un poco.

Así que elimina la comprensión. Es sobrecarga que no es parte de la pregunta:

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

Eso es más como eso! Todavía podemos ser un poco más rápidos si usamos dequepara iterar. Básicamente es lo mismo, pero es más rápido :

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

Lo que me impresiona es que Unicode es competitivo con las cadenas de bytes. Podemos verificar esto explícitamente intentando bytesy unicodeen ambos:

  • 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

    Aquí ves Python 3 en realidad más rápido que 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

    Nuevamente, Python 3 es más rápido, aunque esto es de esperarse ( strha tenido mucha atención en Python 3).

De hecho, esto unicode - bytesla diferencia es muy pequeña, lo cual es impresionante.

Analicemos este caso, ya que es rápido y conveniente para mí:

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

¡De hecho, podemos descartar la respuesta 10 veces votada por Tim Peter!

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

¡Estos no son objetos nuevos!

Pero vale la pena mencionar esto: costos de indexación . La diferencia probablemente estará en la indexación, así que elimine la iteración y solo indexe:

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

La diferencia parece pequeña, pero al menos la mitad del costo es general:

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

entonces la diferencia de velocidad es suficiente para decidir culparla. Yo creo que.

Entonces, ¿por qué indexar una lista es mucho más rápido?

Bueno, volveré a hablar sobre eso, pero supongo que eso se debe a la verificación de cadenas internados (o caracteres en caché si es un mecanismo separado). Esto será menos rápido que óptimo. Pero iré a verificar la fuente (aunque no me siento cómodo en C ...) :).


Así que aquí está la fuente:

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;
}

Caminando desde la cima, tendremos algunos controles. Estos son aburridos. Luego algunas asignaciones, que también deberían ser aburridas. La primera línea interesante es

ch = PyUnicode_READ(kind, data, index);

pero esperamos que sea rápido, ya que estamos leyendo de una matriz C contigua al indexarlo. El resultado, chserá inferior a 256, por lo que devolveremos el carácter almacenado en caché get_latin1_char(ch).

Entonces correremos (soltando los primeros cheques)

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

Dónde

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

(lo cual es aburrido porque las afirmaciones se ignoran en la depuración [para que pueda comprobar que son rápidas] y ((PyASCIIObject *)(op))->state.kind)es (creo) una indirección y un reparto de nivel C);

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

(que también es aburrido por razones similares, suponiendo que las macros ( Something_CAPITALIZED) sean todas rápidas),

#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)] \
        ) \
    ))

(que implica índices pero realmente no es lento en absoluto) y

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;
}

Lo que confirma mi sospecha de que:

  • Esto se almacena en caché:

    PyObject *unicode = unicode_latin1[ch];
  • Esto debería ser rápido. El if (!unicode)no se ejecuta, por lo que es literalmente equivalente en este caso a

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

Honestamente, después de probar que los asserts son rápidos (deshabilitándolos [ creo que funciona en las afirmaciones de nivel C ...]), las únicas partes plausiblemente lentas son:

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

Que son:

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

(rápido, como antes),

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

(rápido si la macro IS_ASCIIes rápida) y

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

(también rápido, ya que es una afirmación más una indirección más un elenco).

Así que estamos abajo (la madriguera del conejo) para:

PyUnicode_IS_ASCII

cual es

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

Hmm ... eso parece rápido también ...


Bueno, está bien, pero comparémoslo con PyList_GetItem. (Sí, gracias Tim Peters por darme más trabajo para hacer: 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];
}

Podemos ver que en casos sin error esto solo se ejecutará:

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

Donde PyList_Checkesta

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

( TABS! TABS !!! ) ( número21587 ) Eso se solucionó y se fusionó en 5 minutos . Como ... si. Maldición. Pusieron a Skeet en vergüenza.

#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

Entonces, esto normalmente es realmente trivial (dos indirecciones y un par de comprobaciones booleanas) a menos que Py_LIMITED_APIesté activado, en cuyo caso ... ???

Luego está la indexación y un elenco ( ((PyListObject *)op) -> ob_item[i]) y hemos terminado.

Por lo tanto, definitivamente hay menos controles para las listas, y las pequeñas diferencias de velocidad ciertamente implican que podría ser relevante.


Creo que, en general, solo hay más verificación de tipo e indirección (->)para Unicode. Parece que me estoy perdiendo un punto, pero ¿qué ?

Veedrac
fuente
17
Estás presentando el código como autoexplicativo; incluso estás presentando los fragmentos como conclusiones. Desafortunadamente para mí, realmente no puedo seguirlo. No digo que su enfoque para descubrir qué está mal no es sólido, pero sería bueno si fuera más fácil de seguir.
PascalVKooten
2
Intenté mejorarlo, pero no estoy seguro de cómo hacerlo más claro. Tenga en cuenta que no escribo C, así que este es un análisis de alto nivel del código y solo los conceptos generales son importantes.
Veedrac
@Nit que he agregado. Dime si te hace falta. Desafortunadamente, también destaca que en realidad no sé la respuesta (* jadeo *).
Veedrac
3
Daré esto otro día antes de aceptar su respuesta (me encantaría ver aparecer algo más concreto), pero gracias por la respuesta muy interesante y bien investigada.
Sunjay Varma
44
Tenga en cuenta que está disparando a un objetivo en movimiento ;-) Esta implementación no solo difiere entre Python 2 y Python 3, sino también entre diferentes versiones. Por ejemplo, en el tronco de desarrollo actual, el get_latin1_char()truco ya no existe unicode_getitem(), sino en el nivel inferior unicode_char. Entonces, hay otro nivel de llamada de función ahora, o no (dependiendo del compilador y los indicadores de optimización utilizados). En este nivel de detalle, simplemente no hay respuestas confiables ;-)
Tim Peters
31

Cuando itera sobre la mayoría de los objetos del contenedor (listas, tuplas, dictos, ...), el iterador entrega los objetos en el contenedor.

Pero cuando itera sobre una cadena, se debe crear un nuevo objeto para cada carácter entregado: una cadena no es "un contenedor" en el mismo sentido que una lista es un contenedor. Los caracteres individuales en una cadena no existen como objetos distintos antes de que la iteración cree esos objetos.

Tim Peters
fuente
3
No creo que esto sea cierto, en realidad. Puedes consultar con is. Que suena bien, pero realmente no creo que puede ser.
Veedrac
Echa un vistazo a la respuesta de @Veedrac.
Cristiano
3
stringobject.cmuestra que __getitem__para cadenas solo recupera el resultado de una tabla de cadenas de 1 carácter almacenadas, por lo que los costos de asignación para esos solo se incurren una vez.
user2357112 es compatible con Monica
10
@ user2357112, sí, para cadenas simples en Python 2 ese es un punto vital. En Python 3, todas las cadenas son "oficialmente" Unicode y hay muchos más detalles involucrados (ver la respuesta de Veedrac). Por ejemplo, en Python 3, después s = chr(256), s is chr(256)regresa False: conocer el tipo solo no es suficiente, porque existen montones de casos especiales debajo de las cubiertas que activan los valores de datos .
Tim Peters
1

Podría incurrir en gastos generales para crear el iterador para la cadena. Mientras que la matriz ya contiene un iterador en la instanciación.

EDITAR:

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

Esto se ejecutó con 2.7, pero en mi mac book pro i7. Esto podría ser el resultado de una diferencia de configuración del sistema.

Robert Chumley
fuente
Incluso solo usando los iteradores rectos, la cadena sigue siendo significativamente más lenta. timeit ("[x para x en él]", "it = iter ('abc')") = 0.34543599384033535; timeit ("[x para x en él]", "it = iter (list ('abc'))") = 0.2791691380446508
Sunjay Varma