Як реалізується список Python?


183

Це пов'язаний список, масив? Я обшукував і лише знаходив людей, які гадали. Мої знання C недостатньо хороші, щоб подивитися на вихідний код.

Відповіді:


58

Це динамічний масив . Практичне підтвердження: індексація займає (звичайно, з надзвичайно малими різницями (0,0013 мкс / с!)) Однаково, незалежно від індексу:

...>python -m timeit --setup="x = [None]*1000" "x[500]"
10000000 loops, best of 3: 0.0579 usec per loop

...>python -m timeit --setup="x = [None]*1000" "x[0]"
10000000 loops, best of 3: 0.0566 usec per loop

Я був би вражений, якби IronPython або Jython використовували пов'язані списки - вони погіршать ефективність багатьох багатьох широко використовуваних бібліотек, побудованих на припущенні, що списки - це динамічні масиви.


1
@Ralf: Я знаю, що мій процесор (більшість інших апаратних засобів теж є на цьому питанні) старий, а собака повільна - з точки зору, я можу припустити, що код, який працює досить швидко, для мене досить швидкий для всіх користувачів: D

88
@delnan: -1 Ваше "практичне підтвердження" - це нісенітниця, як і 6 змін. Близько 98% часу займає робота x=[None]*1000, залишаючи оцінку будь-якої можливої ​​різниці в списку досить неточною. Вам потрібно відокремити ініціалізацію:-s "x=[None]*100" "x[0]"
Джон Махін

26
Показано, що це не наївна реалізація пов'язаного списку. Не остаточно показує, що це масив.
Майкл Міор

6
Про це можна прочитати тут: docs.python.org/2/faq/design.html#how-are-lists-implemented
CCoder

3
Існує набагато більше структур, ніж просто пов'язаний список та масив, тимчасові позначення не корисні для вирішення між ними.
Росс Хемслі

236

Насправді код C досить простий. Розширюючи один макрос і обрізаючи деякі невідповідні коментарі, основна структура знаходиться в listobject.h, яка визначає список як:

typedef struct {
    PyObject_HEAD
    Py_ssize_t ob_size;

    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     */
    Py_ssize_t allocated;
} PyListObject;

PyObject_HEADмістить номер відліку та ідентифікатор типу. Отже, це вектор / масив, який перенаселяє. Код для зміни розміру такого масиву, коли він заповнений, є в listobject.c. Він насправді не подвоює масив, а зростає шляхом виділення

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

до ємності щоразу, де newsizeпотрібний розмір (не обов'язково allocated + 1тому, що ви можете extendза довільною кількістю елементів замість того append, щоб виводити їх один за одним).

Дивіться також поширені питання Python .


6
Отже, ітерація над списками python відбувається так само повільно, як і пов'язані списки, тому що кожен запис є лише вказівником, тому кожен елемент, швидше за все, може спричинити пропуск кешу.
Kr0e

9
@ Kr0e: ні, якщо наступні елементи насправді є тим самим об'єктом :) Але якщо вам потрібні менші структури / більш кешовані структури даних, arrayслід віддавати перевагу модулю або NumPy.
Фред Фоо

@ Kr0e Я б не сказав, що повторення над списком відбувається так само повільно, як і пов'язані списки, але те, що повторення значень зв'язаних списків є повільним, як пов'язаний список, із застереженням, яке згадував Фред. Наприклад, ітерація над списком, щоб скопіювати його до іншого, повинна бути швидшою, ніж пов'язаний список.
Ganea Dan Andrei

35

У CPython списки - це масиви покажчиків. Інші реалізації Python можуть зберігати їх різними способами.


32

Це залежить від реалізації, але IIRC:

  • CPython використовує масив покажчиків
  • Jython використовує ArrayList
  • IronPython, мабуть, також використовує масив. Ви можете переглянути вихідний код, щоб дізнатися це.

Таким чином, всі вони мають O (1) випадковий доступ.


1
Реалізація, залежна від інтерпретатора python, яка реалізує списки як зв'язані списки, була б дійсною реалізацією мови python? Іншими словами: O (1) випадковий доступ до списків не гарантується? Чи не робить це неможливим написання ефективного коду, не покладаючись на деталі реалізації?
sepp2k

2
@sepp Я вважаю, що списки в Python - це лише замовлені колекції; вимоги щодо впровадження та / або виконання зазначеної реалізації прямо не вказані
NullUserException

6
@ sppe2k: Оскільки Python насправді не має стандартних або офіційних специфікацій (хоча є деякі фрагменти документації, які говорять про "... це гарантовано ..."), ви не можете бути на 100% впевнені, як у "цьому" гарантується якийсь аркуш паперу ". Але оскільки O(1)для індексації списків є досить поширеним і справедливим припущенням, жодна реалізація не наважилася б його порушити.

@Paul У ньому нічого не сказано про те, як слід здійснювати базову реалізацію списків.
NullUserException

Просто не буває вказувати великий час роботи речей O. Специфікація синтаксису мови не обов'язково означає те саме, що й деталі реалізації, це часто буває так.
Пол Макміллан

26

Я б запропонував статтю Лорана Люса "Реалізація списку Python" . Мені було дуже корисно, оскільки автор пояснює, як список реалізований у CPython та використовує для цього чудові діаграми.

Перерахуйте структуру об'єкта C

Об'єкт списку в CPython представлений наступною структурою C. ob_item- це список покажчиків на елементи списку. виділено - кількість слотів, виділених у пам'яті.

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

Важливо помітити різницю між виділеними слотами та розмірами списку. Розмір списку такий самий, як len(l). Кількість виділених слотів - це те, що було виділено в пам'яті. Часто ви побачите, що виділений може бути більшим за розмір. Це уникає необхідності виклику reallocкожного разу, коли нові елементи додаються до списку.

...

Додавати

Ми додати ціле число у списку: l.append(1). Що станеться?
введіть тут опис зображення

Ми по- , як і раніше, додавши ще один елемент: l.append(2). list_resizeназивається n + 1 = 2, але оскільки розмір, що виділяється, дорівнює 4, не потрібно виділяти більше пам'яті. Те саме відбувається, коли ми додаємо ще 2 цілих числа: l.append(3), l.append(4). Наступна схема показує, що ми маємо досі.

введіть тут опис зображення

...

Вставити

Вставмо нове ціле число (5) у позицію 1: l.insert(1,5)і подивимось, що відбувається всередині.введіть тут опис зображення

...

Поп

Коли з'являється останній елемент: l.pop(), listpop()називається. list_resizeназивається всередині, listpop()і якщо новий розмір становить менше половини виділеного розміру, тоді список зменшується.введіть тут опис зображення

Ви можете помітити, що слот 4 все ще вказує на ціле число, але найважливішим є розмір списку, який зараз є 4. Давайте ще один елемент. В list_resize(), розмір - 1 = 4 - 1 = 3 менше половини виділених слотів, тому список скорочується до 6 слотів, а новий розмір списку зараз становить 3.

Ви можете помітити, що слоти 3 і 4 все ще вказують на деякі цілі числа, але важливим є розмір списку, який зараз становить 3.введіть тут опис зображення

...

Видалити Python список об'єктів має метод для видалення певного елемента: l.remove(5).введіть тут опис зображення


Дякую, зараз я більше розумію посилання на частину списку. Список Python - це aggregationне, ні composition. Мені б хотілося, щоб був і склад композиції.
шува


5

Як зазначали інші вище, списки (коли помітно великі) реалізуються шляхом виділення фіксованого обсягу простору, і, якщо цей простір повинен заповнити, виділення більшої кількості простору та копіювання на елементи.

Щоб зрозуміти, чому метод амортизований O (1), не втрачаючи загальності, припустимо, що ми вставили a = 2 ^ n елементів, і тепер нам потрібно подвоїти нашу таблицю на розмір 2 ^ (n + 1). Це означає, що в даний час ми робимо 2 ^ (n + 1) операції. Останню копію ми зробили 2 ^ n операцій. До цього ми робили 2 ^ (n-1) ... аж до 8,4,2,1. Тепер, якщо додати їх, отримаємо 1 + 2 + 4 + 8 + ... + 2 ^ (n + 1) = 2 ^ (n + 2) - 1 <4 * 2 ^ n = O (2 ^ n) = O (a) загальна кількість вставок (тобто O (1) амортизований час). Також слід зазначити, що якщо таблиця дозволяє видалити, зменшення таблиці повинно здійснюватися з різним фактором (наприклад, 3x)


Наскільки я розумію, тут немає копіювання старих елементів. Виділяється більше місця, але новий простір не є суміжним з уже використаним простором, і лише нові елементи, які потрібно вставити, копіюються в новий простір. Будь ласка, виправте мене, якщо я помиляюся.
Тушар Вазірані

1

Список в Python - це щось на зразок масиву, де ви можете зберігати кілька значень. Список є змінним, це означає, що ви можете його змінити. Що важливіше, що ви повинні знати, коли ми створюємо список, Python автоматично створює reference_id для цієї змінної списку. Якщо ви зміните її, призначивши інші змінні, головний список буде змінено. Спробуємо з прикладом:

list_one = [1,2,3,4]

my_list = list_one

#my_list: [1,2,3,4]

my_list.append("new")

#my_list: [1,2,3,4,'new']
#list_one: [1,2,3,4,'new']

Ми додаємо, my_listале наш основний список змінився. Цей середній список не призначається як присвоєння списку копій як його посилання.


0

У списку CPython реалізовано як динамічний масив, і тому, коли ми додаємо в цей час, додається не один макрос, а виділяється ще деякий простір, так що кожного разу не слід додавати новий простір.

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