Що змушує [* a] перерозподіляти?


136

Мабуть list(a), не перенастроює, [x for x in a]перераховує в деяких моментах і весь час[*a] розміщує ?

Розміри до n = 100

Ось розміри n від 0 до 12 та отримані розміри в байтах для трьох методів:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

Вираховано так, що відтворюється на repl.it , використовуючи Python 3. 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Отже: Як це працює? Як відбувається [*a]перенапруження? Насправді, який механізм він використовує для створення списку результатів із заданого вводу? Чи використовує він ітератор aі використовує щось подібне list.append? Де вихідний код?

( Співпраця з даними та кодом, який створив зображення.)

Збільшення масштабу до меншої n:

Розміри до n = 40

Зменшення масштабу до n:

Розміри до n = 1000


1
Fwiw, розширюючи свої тестові випадки, здається, що розуміння списку поводиться як написання циклу та додавання кожного елемента до списку, в той час як, [*a]здається, веде себе як використання extendв порожньому списку.
jdehesa

4
Це може допомогти переглянути код байта, сформований для кожного. list(a)працює повністю на С; він може виділяти внутрішній буферний вузол по вузлу в міру ітерації a. [x for x in a]просто використовує LIST_APPENDбагато, тому слід за звичайною схемою "перерозподілити трохи, перерозподілити, коли це необхідно" звичайного списку. [*a]використання BUILD_LIST_UNPACK, яке ... Я не знаю, що це робить, крім, мабуть,
перерозподілу

2
Крім того , в Python 3.7, виявляється , що list(a)і [*a]є ідентичними, і як overallocate по порівнянні з [x for x in a], так що ... sys.getsizeofне може бути правильним інструментом для використання тут.
чепнер

7
@chepner Я думаю, що sys.getsizeofце правильний інструмент, він просто показує, що list(a)використовується для перенаселення. Насправді, що нового в Python 3.8 згадується про це: "Конструктор списку не переназначає [...]" .
Стефан Похман

5
@chepner: помилка виправлена ​​в 3,8 ; конструктор не повинен перерозподіляти.
ShadowRanger

Відповіді:


81

[*a] внутрішньо робить C еквівалент :

  1. Зробіть нове, порожнє list
  2. Дзвінок newlist.extend(a)
  3. Повертається list.

Тож якщо ви розширите тест на:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Спробуйте в Інтернеті!

ви побачите результати для getsizeof([*a])таl = []; l.extend(a); getsizeof(l) однакові.

Зазвичай це правильно робити; коли extendви зазвичай розраховуєте додати більше пізніше, а також для генералізованого розпакування, передбачається, що кілька речей будуть додані одна за одною. [*a]не нормальний випадок; Python передбачає, що до списку додано кілька елементів або ітерабелів list([*a, b, c, *d] ) , тому надмірне розміщення економить роботу у загальному випадку.

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

Що стосується listрозуміння, вони фактично рівнозначні повторюваномуappend s, тому ви бачите кінцевий результат нормальної схеми зростання надмірного розміщення, додаючи елемент за раз.

Щоб було зрозуміло, нічого з цього не є гарантією мови. Це саме те, як CPython реалізує це. Специфікація мови Python, як правило, не стосується конкретних закономірностей зростання list(крім гарантування амортизованих O(1) appends та pops з кінця). Як зазначається в коментарях, конкретна реалізація знову змінюється в 3,9; хоча це не вплине [*a], це може вплинути на інші випадки, коли те, що раніше було "побудувати тимчасові tupleокремі елементи, а потім extendз tuple" тепер стає декількома додатками LIST_APPEND, які можуть змінюватися, коли відбувається перерозподіл та які числа входять у розрахунок.


4
@StefanPochmann: Я раніше читав код (саме тому я вже знав це). Це обробник байтового кодуBUILD_LIST_UNPACK , він використовує _PyList_Extendяк C еквівалент виклику extend(просто безпосередньо, а не за допомогою методу пошуку). Вони поєднували його з доріжками для будівництва tupleз розпакуванням; tuples не перерозподіляти місця для будівництва на шматочках, тому вони завжди розпаковуються до list(щоб отримати вигоду з перерозподілу) і перетворюються tupleв кінці, коли саме це вимагали.
ShadowRanger

4
Зверніть увагу , що це , по- видимому зміна в 3.9 , де будівництво робляться з окремими байткод ( BUILD_LIST, LIST_EXTENDдля кожної речі розпаковувати, LIST_APPENDдля окремих елементів), замість завантаження всього на стеці перед складанням всі listз однією командою байт - коди (це дозволяє компілятор для виконання оптимізацій , що команда все-в-одному не дозволяє, як здійснення , [*a, b, *c]як LIST_EXTEND, LIST_APPEND, LIST_EXTENDж / о необхідності звернути bв одне , tupleщоб відповідати вимогам BUILD_LIST_UNPACK).
ShadowRanger

18

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

Розбирання показує, що BUILD_LIST_UNPACKзвикає:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Це обробляється вceval.c , який створює порожній список і розширює його (з a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend використовує list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Які дзвінки list_resizeз сумою розмірів :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

І це переказує наступним чином:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Давайте перевіримо це. Обчисліть очікувану кількість точок за формулою вище та обчисліть очікуваний розмір байтів, помноживши його на 8 (оскільки я тут використовую 64-розрядний Python) та додавши розмір байтів порожнього списку (тобто постійні накладні витрати об'єкта списку) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Вихід:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Матчі, за винятком тих n = 0, що list_extendнасправді клавіш швидкого доступу , так що насправді і відповідних:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

Це будуть деталі реалізації інтерпретатора CPython, тому вони можуть не відповідати іншим перекладачам.

Це означає, що ви можете побачити, звідки тут розуміння та list(a)поведінка:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

Спеціально для розуміння:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Трохи нижче цих ліній є те, list_preallocate_exactщо використовується під час дзвінка list(a).


1
[*a]не додає окремо окремі елементи. У нього є власний виділений байт-код, який здійснює масове вставлення через extend.
ShadowRanger

Отримав - я думаю, що я не копався досить далеко на цьому. Видалено розділ о[*a]
Ренді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.