Por que é mais lento iterar em uma cadeia pequena do que em uma lista pequena?

132

Eu estava brincando com o timeit e notei que fazer uma simples compreensão da lista em uma pequena sequência levava mais tempo do que fazer a mesma operação em uma lista de pequenas seqüências de caracteres únicas. Alguma explicação? É quase 1,35 vezes mais tempo.

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

O que está acontecendo em um nível inferior que está causando isso?

Sunjay Varma
fonte

Respostas:

193

TL; DR

  • A diferença de velocidade real fica mais próxima de 70% (ou mais) depois que grande parte da sobrecarga é removida, no Python 2.

  • A criação de objetos não é falha. Nenhum dos métodos cria um novo objeto, pois as seqüências de um caracter são armazenadas em cache.

  • A diferença é óbvia, mas provavelmente é criada a partir de um número maior de verificações na indexação de strings, com relação ao tipo e à boa formação. Também é bastante provável, graças à necessidade de verificar o que retornar.

  • A indexação de lista é notavelmente 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

Isso não concorda com o que você encontrou ...

Você deve estar usando o Python 2, então.

>>> 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 explicar a diferença entre as versões. Vou examinar o 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

Você vê aqui que a variante da lista provavelmente será mais lenta devido à criação da lista a cada vez.

Isto é o

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

parte. A variante de sequência possui apenas

 9 LOAD_CONST   3 ('abc')

Você pode verificar se isso parece fazer a diferença:

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

Isso produz apenas

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

como tuplas são imutáveis. Teste:

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

Ótimo, volte à velocidade.

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        

O estranho é que temos o mesmo edifício da lista, mas ainda é mais rápido para isso. Python 2 está agindo estranhamente rápido.

Vamos remover as compreensões e voltar ao tempo. O _ =objetivo é evitar que ele seja otimizado.

>>> 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 a inicialização não é significativa o suficiente para explicar a diferença entre as versões (esses números são pequenos)! Assim, podemos concluir que o Python 3 tem uma compreensão mais lenta. Isso faz sentido, pois o Python 3 mudou as compreensões para ter um escopo mais seguro.

Bem, agora melhore o benchmark (estou apenas removendo a sobrecarga que não é iteração). Isso remove a construção do iterável pré-atribuindo-o:

>>> 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 se a chamada iteré a 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

Não. Não, não é. A diferença é muito pequena, especialmente para o Python 3.

Então, vamos remover ainda mais sobrecargas indesejadas ... tornando a coisa mais lenta! O objetivo é apenas ter uma iteração mais longa, para que o tempo oculte sobrecarga.

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

Na verdade , isso não mudou muito , mas ajudou um pouco.

Portanto, remova a compreensão. É uma sobrecarga que não faz parte da questão:

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

É mais assim! Ainda podemos ficar um pouco mais rápidos usando dequea iteração. É basicamente o mesmo, mas é mais 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

O que me impressiona é que o Unicode é competitivo com bytestrings. Podemos verificar isso explicitamente, tentando bytese unicodeem 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
    

    Aqui você vê o Python 3 realmente mais rápido que o 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
    

    Mais uma vez, o Python 3 é mais rápido, embora isso seja esperado ( strteve muita atenção no Python 3).

Na verdade, isso unicode- bytesdiferença é muito pequena, o que é impressionante.

Então, vamos analisar este caso, visto que é rápido e conveniente para mim:

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

Na verdade, podemos descartar a resposta 10 vezes mais votada de Tim Peter!

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

Estes não são novos objetos!

Mas vale a pena mencionar: indexação de custos . A diferença provavelmente estará na indexação, então remova a iteração e apenas 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

A diferença parece pequena, mas pelo menos metade do custo é indireto:

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

então a diferença de velocidade é suficiente para decidir culpar. Eu acho que.

Então, por que a indexação de uma lista é muito mais rápida?

Bem, voltarei a você sobre isso, mas o meu palpite é que isso depende da verificação de cadeias internas (ou caracteres em cache, se for um mecanismo separado). Isso será menos rápido que o ideal. Mas vou verificar a fonte (embora não esteja confortável em C ...) :).


Então aqui está a fonte:

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

Andando de cima, teremos alguns cheques. Estes são chatos. Em seguida, algumas atribuições, que também devem ser chatas. A primeira linha interessante é

ch = PyUnicode_READ(kind, data, index);

mas esperamos que seja rápido, pois estamos lendo de uma matriz C contígua ao indexá-la. O resultado, chserá menor que 256, portanto, retornaremos o caractere em cache get_latin1_char(ch).

Então, vamos executar (descartando as primeiras verificações)

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

Onde

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

(o que é chato porque as afirmações são ignoradas na depuração [para que eu possa verificar se elas são rápidas] e ((PyASCIIObject *)(op))->state.kind)é (acho) um indireto e uma transmissão no nível C);

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

(o que também é chato por razões semelhantes, supondo que as macros ( Something_CAPITALIZED) sejam 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 envolve índices, mas realmente não é lento) e

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

O que confirma minha suspeita de que:

  • Este é armazenado em cache:

    PyObject *unicode = unicode_latin1[ch];
  • Isso deve ser rápido. O if (!unicode)não é executado, portanto, neste caso, é literalmente equivalente a

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

Honestamente, depois de testar, os asserts são rápidos (desativando-os [ acho que funciona com as afirmações de nível C ...]), as únicas partes plausivelmente lentas são:

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

Que são:

#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 se a macro IS_ASCIIfor rápida) e

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

(também rápido, pois é uma afirmação mais um indireto mais um elenco).

Então, nós estamos (no buraco do coelho) em:

PyUnicode_IS_ASCII

qual é

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

Hmm ... isso parece rápido também ...


Bem, ok, mas vamos comparar PyList_GetItem. (Sim, obrigado Tim Peters por me dar mais trabalho a fazer: 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, em casos sem erro, isso apenas será executado:

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

Onde PyList_Checkfica

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

( TABS! TABS !!! ) ( edição21587 ) Isso foi corrigido e mesclado em 5 minutos . Tipo ... sim. Droga. Eles envergonham Skeet.

#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

Portanto, isso normalmente é realmente trivial (duas indiretas e algumas verificações booleanas), a menos que Py_LIMITED_APIesteja ativado, nesse caso ... ???

Depois, há a indexação e um cast ( ((PyListObject *)op) -> ob_item[i]) e terminamos.

Portanto, há definitivamente menos verificações de listas, e as pequenas diferenças de velocidade certamente significam que isso pode ser relevante.


Eu acho que, em geral, há apenas mais verificação de tipo e indireto (->)para Unicode. Parece que estou perdendo um ponto, mas o que ?

Veedrac
fonte
17
Você está apresentando o código como auto-explicativo; você está até apresentando os trechos como conclusões. Infelizmente para mim, eu realmente não posso segui-lo. Não estou dizendo que sua abordagem para descobrir o que está errado não é sólida, mas seria bom se fosse mais fácil de seguir.
PascalVKooten
2
Tentei melhorá-lo, mas não tenho certeza de como torná-lo mais claro. Observe que eu não escrevo C, portanto essa é uma análise de alto nível do código e apenas os conceitos gerais são importantes.
Veedrac
@ Nit eu adicionei. Diga-me se parece que falta. Infelizmente, também destaca que eu realmente não sei a resposta (* suspiro *).
Veedrac
3
Darei isso outro dia antes de aceitar sua resposta (eu adoraria ver algo mais concreto aparecer), mas obrigado pela resposta muito interessante e bem pesquisada.
Sunjay Varma
4
Observe que você está atirando em um alvo em movimento ;-) Essa implementação não difere apenas entre Python 2 e Python 3, mas também entre versões diferentes. Por exemplo, no tronco de desenvolvimento atual, o get_latin1_char()truque não existe mais unicode_getitem(), mas no nível inferior unicode_char. Portanto, há outro nível de chamada de função agora - ou não (dependendo dos compiladores e sinalizadores de otimização usados). Neste nível de detalhe, simplesmente não há respostas confiáveis ;-)
Tim Peters
31

Quando você itera sobre a maioria dos objetos de contêiner (listas, tuplas, dictos, ...), o iterador entrega os objetos no contêiner.

Mas quando você itera sobre uma string, um novo objeto deve ser criado para cada caractere entregue - uma string não é "um contêiner" no mesmo sentido em que uma lista é um contêiner. Os caracteres individuais em uma sequência não existem como objetos distintos antes da iteração criar esses objetos.

Tim Peters
fonte
3
Eu não acho que isso seja verdade. Você pode verificar com is. Ele soa bem, mas eu realmente não acho que ele pode ser.
Veedrac
Dê uma olhada na resposta do @Veedrac.
Christian
3
stringobject.cmostra que __getitem__para strings apenas recupera o resultado de uma tabela de strings de 1 caractere armazenadas, portanto os custos de alocação são incorridos apenas uma vez.
User2357112 suporta Monica
10
@ user2357112, sim, para strings simples no Python 2, esse é um ponto vital. No Python 3, todas as strings são Unicode "oficialmente" e muito mais detalhes estão envolvidos (consulte a resposta de Veedrac). Por exemplo, no Python 3, depois s = chr(256), s is chr(256)retorna False- conhecer o tipo por si só não é suficiente, porque existem montes de casos especiais sob as cobertas, acionando os valores dos dados .
Tim Peters
1

Você pode estar incorrendo e sobrecarregando a criação do iterador para a sequência. Enquanto a matriz já contém um iterador na instanciação.

EDITAR:

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

Isso foi executado usando 2.7, mas no meu mac book pro i7. Isso pode ser o resultado de uma diferença de configuração do sistema.

Robert Chumley
fonte
Mesmo usando apenas os iteradores retos, a sequência ainda é significativamente mais lenta. timeit ("[x para x nele]", "it = iter ('abc')") = 0,34543599384033535; timeit ("[x para x nele]", "it = iter (lista ('abc'))") = 0,2971691380446508
Sunjay Varma