Python: Списки
Теория: Массивы в памяти компьютера
Работая на таких высокоуровневых языках, как Python, позволительно не знать устройство списков для решения повседневных задач. С другой стороны, подобное понимание делает код менее магическим и дает возможность заглядывать чуть дальше.
Массивы в Cи
В основе списка лежит массив. Реальные массивы лучше всего рассматривать на языке Cи, который, с одной стороны, достаточно простой и понятный, с другой — очень близок к железу и не скрывает от нас практически ничего. Когда мы говорим про примитивные типы данных, такие как "строка" или "число", то на интуитивном уровне все довольно понятно: под каждое значение выделяется некоторый размер памяти в соответствии с типом, в которой и хранится само значение. А как должна выделиться память под хранение массива? И что такое массив в памяти? На уровне хранения понятия массив не существует. Массив представляется цельным куском памяти, размер которого вычисляется по следующей формуле: количество элементов * количество памяти под каждый элемент. Из этого утверждения есть два интересных вывода:
- Размер массива — фиксированная величина. Те динамические массивы (изменяющие свой размер во время работы), с которыми мы имеем дело во многих языках, реализованы уже внутри языка, а не на уровне железа.
- Все элементы массива имеют один тип и занимают одно и то же количество памяти. Благодаря этому появляется возможность простым умножением (по формуле, описанной выше) получить адрес той ячейки, в которой лежит нужный нам элемент. Именно это происходит под капотом, при обращении к элементу массива под определенным индексом.
Фактически индекс в массиве — смещение относительно начала куска памяти, содержащего данные массива. Адрес, по которому расположен элемент под конкретным индексом, рассчитывается так: индекс * количество памяти, занимаемое одним элементом (для данного типа данных на данной архитектуре). Пример на Си:
Если предположить, что тип int занимает в памяти 2 байта, то адрес элемента, соответствующего индексу 3, вычисляется так: начальный адрес + 3 * 2. Начальный адрес — это адрес ячейки памяти, начиная с которой располагается массив. Он формируется во время выделения памяти под массив. Ниже пример расчета адресов памяти под разные элементы массива numbers:
Теперь должно быть понятно, почему индексы в массиве начинаются с нуля. 0 — означает отсутствие смещения.
Но не все данные имеют одинаковый размер. Как будет храниться массив строк? Строки имеют разную длину и требуют разное количество памяти для своего хранения. Один из способов сохранить строки в массиве на языке Си – создать массив списков (тут нужно понимать, что любая строка в Си это массив символов). Вложенные массивы обязательно должны быть одного размера, невозможно обойти физические ограничения списков. Хитрость в том, что этот размер должен быть достаточно большой, чтобы туда поместились необходимые строки.
Безопасность
В отличие от высокоуровневых языков, в которых код защищен от выхода за границу массива, в таком языке, как C, выход за границу не приводит к ошибкам (на самом деле он может приводить к segfault, но это здесь не важно). Обращение к элементу, индекс которого находится за пределами массива, вернет данные, которые лежат в той самой области памяти, куда его попросили обратиться в соответствии с формулой выше. Чем они окажутся — никому не известно (но они будут проинтерпретированы в соответствии с типом массива. Если массив имеет тип int, то вернется число). Выход за границу массива активно эксплуатируется хакерами для взлома программ.
Массивы в динамических языках
В динамических языках, таких как Python, устройство списков значительно сложнее, чем в си. Так как типы данных вычисляются автоматически во время выполнения кода. Массив в такой среде не может работать, как в языке C. Неизвестно, данные каких типов окажутся внутри в процессе работы.
Массивы в таких языках содержат не сами данные, а ссылки (адреса в памяти) на них. Тогда становится не важно, что хранить. Любое значение в массиве – адрес, имеющий одинаковый размер независимо от данных, на которые он указывает. Такой подход делает массивы гибкими, но с другой стороны, более медленными.
Кроме того, массивы в динамических языках тоже динамические. То есть их размер может увеличиваться или уменьшаться в процессе работы программы. Технически это работает так: если ссылки (помним, что данные там не хранятся) в массив не помещаются, то интерпретатор внутри себя создает новый массив большего размера (обычно в два раза) и переносит все ссылки туда. Динамические массивы очень упрощают процесс разработки, но за это тоже приходится платить скоростью.







