Mengapa lebih lambat untuk beralih pada string kecil daripada daftar kecil?

132

Saya bermain-main dengan timeit dan memperhatikan bahwa melakukan pemahaman daftar sederhana atas string kecil membutuhkan waktu lebih lama daripada melakukan operasi yang sama pada daftar string karakter tunggal kecil. Ada penjelasan? Ini hampir 1,35 kali lebih banyak waktu.

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

Apa yang terjadi pada level bawah yang menyebabkan ini?

Sunjay Varma
sumber

Jawaban:

193

TL; DR

  • Perbedaan kecepatan aktual lebih dekat ke 70% (atau lebih) begitu banyak overhead dihapus, untuk Python 2.

  • Penciptaan objek tidak bersalah. Tidak ada metode yang menciptakan objek baru, karena string satu karakter di-cache.

  • Perbedaannya tidak jelas, tetapi kemungkinan dibuat dari sejumlah besar pemeriksaan pada pengindeksan string, berkaitan dengan jenis dan pembentukan yang baik. Ini juga kemungkinan besar berkat kebutuhan untuk memeriksa apa yang harus dikembalikan.

  • Daftar indeks sangat cepat.



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

Ini tidak setuju dengan apa yang Anda temukan ...

Anda harus menggunakan Python 2, lalu.

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

Mari kita jelaskan perbedaan antar versi. Saya akan memeriksa kode yang dikompilasi.

Untuk 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

Anda lihat di sini bahwa varian daftar cenderung lebih lambat karena pembangunan daftar setiap kali.

Ini adalah

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

bagian. Varian string hanya memiliki

 9 LOAD_CONST   3 ('abc')

Anda dapat memeriksa apakah ini tampaknya membuat perbedaan:

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

Ini menghasilkan adil

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

sebagai tuple tidak berubah. Uji:

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

Bagus, kembali ke kecepatan.

Untuk 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        

Yang aneh adalah kami memiliki daftar yang sama , tetapi masih lebih cepat untuk ini. Python 2 bertingkah anehnya cepat.

Mari kita hilangkan pemahaman dan waktu ulang. Ini _ =untuk mencegahnya dioptimalkan.

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

Kita dapat melihat bahwa inisialisasi tidak cukup signifikan untuk menjelaskan perbedaan antara versi (angka-angka itu kecil)! Dengan demikian kita dapat menyimpulkan bahwa Python 3 memiliki pemahaman yang lebih lambat. Ini masuk akal karena Python 3 mengubah pemahaman untuk memiliki pelingkupan yang lebih aman.

Nah, sekarang perbaiki patokannya (saya hanya menghapus overhead yang bukan iterasi). Ini menghapus bangunan iterable dengan pra-menugaskannya:

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

Kami dapat memeriksa apakah panggilan iteradalah biaya overhead:

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

Tidak, tidak. Perbedaannya terlalu kecil, terutama untuk Python 3.

Jadi mari kita hilangkan overhead yang tidak diinginkan ... dengan membuat semuanya lebih lambat! Tujuannya hanya untuk memiliki iterasi yang lebih lama sehingga waktu bersembunyi di atas kepala.

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

Ini sebenarnya tidak banyak berubah , tetapi sedikit membantu.

Jadi hilangkan pemahamannya. Ada overhead yang bukan bagian dari pertanyaan:

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

Itu lebih seperti itu! Kita bisa mendapatkan sedikit lebih cepat dengan menggunakan dequeiterate. Pada dasarnya sama, tetapi lebih cepat :

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

Yang mengesankan bagi saya adalah bahwa Unicode bersaing dengan bytestrings. Kami dapat memeriksa ini secara eksplisit dengan mencoba bytesdan unicodekeduanya:

  • 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

    Di sini Anda melihat Python 3 sebenarnya lebih cepat dari 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

    Sekali lagi, Python 3 lebih cepat, meskipun ini yang diharapkan ( strtelah memiliki banyak perhatian di Python 3).

Sebenarnya, ini unicode- bytesperbedaannya sangat kecil, yang mengesankan.

Jadi mari kita menganalisis satu kasus ini, mengingat cepat dan nyaman bagi saya:

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

Kami benar-benar dapat mengesampingkan jawaban Tim Peter yang 10 kali lipat!

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

Ini bukan benda baru!

Tapi ini layak disebutkan: biaya pengindeksan . Perbedaannya kemungkinan terletak pada pengindeksan, jadi hapus iterasi dan indeks saja:

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

Perbedaannya tampak kecil, tetapi setidaknya setengah dari biaya 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

jadi perbedaan kecepatan cukup untuk memutuskan untuk menyalahkannya. Kupikir.

Jadi mengapa pengindeksan daftar jauh lebih cepat?

Yah, saya akan kembali kepada Anda tentang hal itu, tetapi dugaan saya adalah itu tergantung pada cek untuk string yang diinternir (atau karakter yang di-cache jika ini merupakan mekanisme yang terpisah). Ini akan kurang cepat dari optimal. Tapi saya akan memeriksa sumbernya (walaupun saya tidak nyaman di C ...) :).


Jadi, inilah sumbernya:

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

Berjalan dari atas, kami akan periksa. Ini membosankan. Kemudian beberapa tugas, yang juga harus membosankan. Baris pertama yang menarik adalah

ch = PyUnicode_READ(kind, data, index);

tapi kami berharap itu cepat, karena kami membaca dari array C yang berdekatan dengan mengindeksnya. Hasilnya,, chakan kurang dari 256 sehingga kami akan mengembalikan karakter yang di-cache get_latin1_char(ch).

Jadi kita akan lari (menjatuhkan cek pertama)

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

Dimana

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

(Yang membosankan karena menegaskan diabaikan dalam debug [sehingga saya dapat memeriksa bahwa mereka cepat] dan ((PyASCIIObject *)(op))->state.kind)(saya pikir) adalah tipuan dan pemain tingkat C);

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

(Yang juga membosankan karena alasan yang sama, dengan asumsi makro ( Something_CAPITALIZED) semuanya cepat),

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

(yang melibatkan indeks tetapi sebenarnya tidak lambat sama sekali) dan

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

Yang menegaskan kecurigaan saya bahwa:

  • Ini di-cache:

    PyObject *unicode = unicode_latin1[ch];
  • Ini harus cepat. Ini if (!unicode)tidak dijalankan, jadi secara harfiah setara dalam hal ini

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

Jujur, setelah menguji asserts cepat (dengan menonaktifkannya [saya pikir itu bekerja pada C-level menegaskan ...]), satu-satunya bagian yang masuk akal-lambat adalah:

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

Yang mana:

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

(cepat, seperti sebelumnya),

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

(cepat jika makro IS_ASCIIcepat), dan

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

(Juga secepat itu menegaskan ditambah tipuan ditambah pemain).

Jadi kita turun (lubang kelinci) ke:

PyUnicode_IS_ASCII

yang mana

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

Hmm ... itu sepertinya cepat juga ...


Baiklah, tapi mari kita bandingkan PyList_GetItem. (Ya, terima kasih Tim Peters karena memberi saya lebih banyak pekerjaan untuk dilakukan: 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];
}

Kita dapat melihat bahwa pada kasus non-kesalahan ini hanya akan berjalan:

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

dimana PyList_Checkadalah

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

( TABS! TABS !!! ) ( issue21587 ) Itu diperbaiki dan digabung dalam 5 menit . Seperti ... ya. Sial. Mereka mempermalukan 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

Jadi ini biasanya sangat sepele (dua tipuan dan beberapa cek boolean) kecuali Py_LIMITED_APIaktif, dalam hal ini ... ???

Lalu ada pengindeksan dan pemeran ( ((PyListObject *)op) -> ob_item[i]) dan kami selesai.

Jadi pasti ada lebih sedikit pemeriksaan untuk daftar, dan perbedaan kecepatan kecil tentu menyiratkan bahwa itu mungkin relevan.


Saya pikir secara umum, hanya ada lebih banyak pengecekan tipe dan tipuan (->)untuk Unicode. Sepertinya saya kehilangan satu poin, tapi apa ?

Veedrac
sumber
17
Anda menyajikan kode sebagai penjelasan sendiri; Anda bahkan menyajikan potongan sebagai kesimpulan. Sayangnya bagi saya, saya tidak bisa mengikutinya. Tidak mengatakan bahwa pendekatan Anda untuk mencari tahu apa yang salah itu tidak solid, tetapi alangkah baiknya jika itu lebih mudah diikuti.
PascalVKooten
2
Saya mencoba meningkatkannya, tetapi saya tidak yakin bagaimana membuatnya lebih jelas. Perhatikan bahwa saya tidak menulis C, jadi ini adalah analisis kode tingkat tinggi dan hanya konsep keseluruhan yang penting.
Veedrac
@Nit, saya sudah menambahkan. Katakan padaku jika rasanya kurang. Sayangnya itu juga menyoroti bahwa saya tidak benar-benar tahu jawabannya (* terkesiap *).
Veedrac
3
Saya akan memberikan ini satu hari lagi sebelum saya menerima jawaban Anda (saya ingin melihat sesuatu yang lebih konkret muncul), tetapi terima kasih atas jawaban yang sangat menarik dan diteliti dengan baik.
Sunjay Varma
4
Perhatikan bahwa Anda menembak target bergerak ;-) Implementasi ini tidak hanya berbeda antara Python 2 dan Python 3, tetapi juga antara rilis yang berbeda. Misalnya, pada trunk pengembangan saat ini, get_latin1_char()triknya tidak ada lagi unicode_getitem(), tetapi di level bawah unicode_char. Jadi ada level lain dari pemanggilan fungsi sekarang - atau tidak (tergantung pada kompiler dan flag optimasi yang digunakan). Pada tingkat perincian ini, sama sekali tidak ada jawaban yang dapat diandalkan ;-)
Tim Peters
31

Ketika Anda beralih pada sebagian besar objek kontainer (daftar, tupel, dikte, ...), iterator mengirimkan objek dalam wadah.

Tetapi ketika Anda beralih pada string, objek baru harus dibuat untuk setiap karakter yang disampaikan - string bukan "wadah" dalam arti yang sama daftar adalah wadah. Karakter individu dalam string tidak ada sebagai objek yang berbeda sebelum iterasi membuat objek tersebut.

Tim Peters
sumber
3
Saya kira ini tidak benar. Anda bisa memeriksanya is. Ini terdengar benar, tapi aku benar-benar tidak berpikir itu bisa.
Veedrac
Lihatlah jawaban @Veedrac.
Christian
3
stringobject.cmenunjukkan bahwa __getitem__untuk string hanya mengambil hasil dari tabel string 1-karakter yang disimpan, sehingga biaya alokasi untuk itu hanya dikeluarkan satu kali.
user2357112 mendukung Monica
10
@ user2357112, ya, untuk string sederhana dalam Python 2 itu adalah poin penting. Dalam Python 3, semua string "Unicode" resmi "dan lebih banyak detail terlibat (lihat jawaban Veedrac). Misalnya, di Python 3, setelah s = chr(256), s is chr(256)kembali False- mengetahui jenis saja tidak cukup, karena gundukan kasus khusus ada di bawah selimut memicu pada data nilai .
Tim Peters
1

Anda bisa dikenakan dan overhead untuk membuat iterator untuk string. Sedangkan array sudah berisi iterator setelah instantiation.

EDIT:

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

Ini dijalankan menggunakan 2.7, tetapi di mac book saya pro i7. Ini bisa jadi hasil dari perbedaan konfigurasi sistem.

Robert Chumley
sumber
Bahkan hanya menggunakan iterator lurus, string masih jauh lebih lambat. timeit ("[x untuk x di dalamnya]", "it = iter ('abc')") = 0,34543599384033535; timeit ("[x untuk x di dalamnya]", "it = iter (daftar ('abc'))") = 0,2791691380446508
Sunjay Varma