Perché è più lento iterare su una stringa piccola rispetto a un elenco piccolo?


132

Stavo giocando con il tempo e notai che fare una semplice comprensione della lista su una piccola stringa richiedeva più tempo che fare la stessa operazione su una lista di piccole stringhe a singolo carattere. Alcuna spiegazione? È quasi 1,35 volte più tempo.

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

Cosa sta succedendo a un livello inferiore che sta causando questo?

Risposte:


193

TL; DR

  • La differenza di velocità effettiva è più vicina al 70% (o più) una volta rimosso un sacco di sovraccarico, per Python 2.

  • La creazione di oggetti non è in errore. Nessuno dei due metodi crea un nuovo oggetto, poiché le stringhe di un carattere vengono memorizzate nella cache.

  • La differenza è evidente, ma è probabilmente creata da un maggior numero di controlli sull'indicizzazione delle stringhe, per quanto riguarda il tipo e la buona formazione. È anche molto probabile grazie alla necessità di controllare cosa restituire.

  • L'indicizzazione delle liste è notevolmente veloce.



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

Questo non è d'accordo con quello che hai trovato ...

Quindi devi usare 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

Spieghiamo la differenza tra le versioni. Esaminerò il codice compilato.

Per 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

Vedete qui che la variante dell'elenco sarà probabilmente più lenta a causa della costruzione dell'elenco ogni volta.

Questo è il

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

parte. La variante di stringa ha solo

 9 LOAD_CONST   3 ('abc')

Puoi controllare che questo sembra fare la differenza:

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

Questo produce giusto

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

poiché le tuple sono immutabili. Test:

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

Fantastico, torna alla velocità.

Per 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        

La cosa strana è che abbiamo lo stesso edificio della lista, ma è ancora più veloce per questo. Python 2 agisce in modo stranamente veloce.

Rimuoviamo le comprensioni e ri-tempo. Lo scopo _ =è evitare che venga ottimizzato.

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

Possiamo vedere che l'inizializzazione non è abbastanza significativa da tenere conto della differenza tra le versioni (quei numeri sono piccoli)! Possiamo quindi concludere che Python 3 ha una comprensione più lenta. Questo ha senso dato che Python 3 ha cambiato comprensione per avere un ambito più sicuro.

Bene, ora migliora il benchmark (sto solo rimuovendo le spese generali che non sono iterazioni). Questo rimuove la costruzione dell'iterabile pre-assegnandolo:

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

Possiamo verificare se chiamare iterè il sovraccarico:

>>> 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 non lo è. La differenza è troppo piccola, specialmente per Python 3.

Quindi rimuoviamo un sovraccarico ancora più indesiderato ... rendendo tutto più lento! L'obiettivo è solo di avere un'iterazione più lunga in modo che il tempo si nasconda in alto.

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

Questo in realtà non è cambiato molto , ma ha aiutato un po '.

Quindi rimuovere la comprensione. È sovraccarico che non fa parte della domanda:

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

È più simile! Possiamo ottenere ancora leggermente più velocemente usando dequeiterate. È praticamente lo stesso, ma è più veloce :

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

Ciò che mi colpisce è che Unicode è competitivo con i bytestring. Possiamo verificarlo esplicitamente provando bytese unicodein entrambi:

  • 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
    

    Qui vedi Python 3 in realtà più veloce di 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
    

    Ancora una volta, Python 3 è più veloce, anche se è prevedibile ( strha avuto molta attenzione in Python 3).

In realtà, questo unicode- bytesdifferenza è molto piccola, che è impressionante.

Analizziamo quindi questo caso, visto che è veloce e conveniente per me:

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

Possiamo davvero escludere la risposta 10 volte più votata di Tim Peter!

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

Questi non sono nuovi oggetti!

Ma vale la pena ricordare: i costi di indicizzazione . La differenza sarà probabilmente nell'indicizzazione, quindi rimuovi l'iterazione e indicizza solo:

>>> 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 differenza sembra piccola, ma almeno la metà del costo è overhead:

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

quindi la differenza di velocità è sufficiente per decidere di biasimarla. Penso.

Quindi perché indicizzare un elenco è molto più veloce?

Bene, tornerò da te su questo, ma la mia ipotesi è che dipende dal controllo delle stringhe internate (o dei caratteri memorizzati nella cache se si tratta di un meccanismo separato). Questo sarà meno veloce che ottimale. Ma andrò a controllare la fonte (anche se non mi sento a mio agio in C ...) :).


Quindi ecco la 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;
}

Camminando dall'alto, avremo alcuni controlli. Questi sono noiosi. Quindi alcuni incarichi, che dovrebbero anche essere noiosi. La prima linea interessante è

ch = PyUnicode_READ(kind, data, index);

ma speriamo che sia veloce, mentre stiamo leggendo da un array C contiguo indicizzandolo. Il risultato chsarà inferiore a 256, quindi restituiremo il carattere memorizzato nella cache get_latin1_char(ch).

Quindi correremo (lasciando cadere i primi controlli)

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

Dove

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

(il che è noioso perché le asserzioni vengono ignorate nel debug [quindi posso verificare che siano veloci] ed ((PyASCIIObject *)(op))->state.kind)è (penso) un riferimento indiretto e un cast di livello C);

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

(che è anche noioso per ragioni simili, supponendo che le macro ( Something_CAPITALIZED) siano tutte veloci),

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

(che coinvolge gli indici ma non è affatto 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;
}

Il che conferma il mio sospetto che:

  • Questo è memorizzato nella cache:

    PyObject *unicode = unicode_latin1[ch];
  • Questo dovrebbe essere veloce. Non if (!unicode)viene eseguito, quindi in questo caso è letteralmente equivalente

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

Onestamente, dopo aver testato gli asserts sono veloci (disabilitandoli [penso che funzioni sulle asserzioni di livello C ...]), le uniche parti plausibilmente lente sono:

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

Quali sono:

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

(veloce, come prima),

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

(veloce se la macro IS_ASCIIè veloce) e

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

(anche veloce in quanto è un'asserzione più un riferimento indiretto più un cast).

Quindi siamo giù (la tana del coniglio) per:

PyUnicode_IS_ASCII

che è

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

Hmm ... anche quello sembra veloce ...


Bene, ok, ma confrontiamolo con PyList_GetItem. (Sì, grazie Tim Peters per avermi dato più lavoro da fare: 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];
}

Possiamo vedere che in casi non di errore questo funzionerà:

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

dove PyList_Checksi trova

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

( TABS! TABS !!! ) ( issue21587 ) È stato risolto e unito in 5 minuti . Come ... sì. Dannazione. Hanno fatto vergognare 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

Quindi questo è normalmente banale (due indirette e un paio di controlli booleani) a meno che non Py_LIMITED_APIsia attivo, nel qual caso ... ???

Poi c'è l'indicizzazione e un cast ( ((PyListObject *)op) -> ob_item[i]) e abbiamo finito.

Quindi ci sono sicuramente meno controlli per gli elenchi e le piccole differenze di velocità implicano sicuramente che potrebbe essere rilevante.


Penso in generale, c'è solo più controllo del tipo e indiretta (->)per Unicode. Sembra che mi manchi un punto, ma cosa ?


17
Stai presentando il codice come autoesplicativo; stai anche presentando i frammenti come conclusioni. Sfortunatamente per me, non riesco davvero a seguirlo. Non dire che il tuo approccio alla scoperta di ciò che è sbagliato non è solido, ma sarebbe bello se fosse più facile da seguire.
PascalVKooten,

2
Ho provato a migliorarlo, ma non sono sicuro su come renderlo più chiaro. Nota che non scrivo C, quindi questa è un'analisi di alto livello del codice e solo i concetti generali sono importanti.
Veedrac,

@No ho aggiunto. Dimmi se ti sembra carente. Sfortunatamente sottolinea anche che in realtà non conosco la risposta (* gasp *).
Veedrac,

3
Lo darò un altro giorno prima di accettare la tua risposta (mi piacerebbe vedere apparire qualcosa di più concreto), ma grazie per la risposta molto interessante e ben studiata.
Sunjay Varma,

4
Nota che stai sparando a un bersaglio in movimento ;-) Questa implementazione non differisce solo tra Python 2 e Python 3, ma anche tra diverse versioni. Ad esempio, nell'attuale trunk di sviluppo, il get_latin1_char()trucco non esiste più unicode_getitem(), ma nel livello inferiore unicode_char. Quindi ora c'è un altro livello di chiamata di funzione - oppure no (a seconda del compilatore e dei flag di ottimizzazione utilizzati). A questo livello di dettaglio, semplicemente non ci sono risposte affidabili ;-)
Tim Peters,

31

Quando si esegue l'iterazione sulla maggior parte degli oggetti contenitore (elenchi, tuple, dadi, ...), l'iteratore consegna gli oggetti nel contenitore.

Ma quando si scorre su una stringa, è necessario creare un nuovo oggetto per ogni carattere consegnato - una stringa non è "un contenitore" nello stesso senso in cui un elenco è un contenitore. I singoli caratteri in una stringa non esistono come oggetti distinti prima che l'iterazione crei tali oggetti.


3
Non penso che sia vero, in realtà. Puoi verificare con is. Si suona bene, ma io in realtà non credo che possa essere.
Veedrac,

Dai un'occhiata alla risposta @Veedrac.
Christian,

3
stringobject.cmostra che __getitem__per le stringhe recupera semplicemente il risultato da una tabella di stringhe di 1 carattere memorizzate, quindi i costi di allocazione per quelli sono sostenuti solo una volta.
user2357112 supporta Monica

10
@ user2357112, sì, per stringhe semplici in Python 2 questo è un punto vitale. In Python 3, tutte le stringhe sono "ufficialmente" Unicode e sono coinvolti molti più dettagli (vedi la risposta di Veedrac). Ad esempio, in Python 3, dopo s = chr(256), s is chr(256)ritorna False- conoscere il solo tipo non è sufficiente, perché sotto le copertine esistono innumerevoli casi speciali che innescano i valori dei dati .
Tim Peters,

1

Potresti essere incorso e sovraccarico per la creazione dell'iteratore per la stringa. Mentre l'array contiene già un iteratore all'istanza.

MODIFICARE:

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

Questo è stato eseguito utilizzando 2.7, ma sul mio Mac Book Pro i7. Questo potrebbe essere il risultato di una differenza di configurazione del sistema.


Anche solo usando gli iteratori diritti, la stringa è ancora significativamente più lenta. timeit ("[x per x in it]", "it = iter ('abc')") = 0.34543599384033535; timeit ("[x for x in it]", "it = iter (list ('abc'))") = 0.2791691380446508
Sunjay Varma,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.