common — общие утилиты¶
Базовые helper'ы, которые повторяются из лабораторной в лабораторную, вынесены в локальный пакет labs/common — чтобы не дублировать один и тот же код. Лабы импортируют их напрямую:
Импортировать можно как из пакета (from labs.common import array_length), так и из конкретного модуля (from labs.common.arrays import array_length). Встроенные функции (len, range, …) внутри этих helper'ов намеренно не используются — это часть учебного задания.
Пакет разложен на два модуля:
| Модуль | Что внутри |
|---|---|
labs.common.arrays |
array_length, generate_array, print_array |
labs.common.ranges |
CustomRange (класс) и псевдоним custom_range |
arrays — операции над массивом¶
array_length(arr) — длина без len()¶
Проходим по всем элементам и считаем их — «ручная» замена len().
def array_length(arr: list) -> int:
count = 0
for _ in arr: # перебираем элементы, само значение не нужно
count += 1
return count
- Сложность: O(n) — один проход.
- В реальном коде так писать не нужно: есть
len()за O(1). Здесь — учебная цель.
generate_array(size) — заполнение случайными числами¶
def generate_array(size: int) -> list[int]:
result = []
i = 0
while i < size:
result.append(random.randint(1, 100)) # случайное число 1..100 включительно
i += 1
return result
random.randint(a, b)возвращает целое в диапазоне [a, b] (обе границы включены).- Цикл
whileс ручным счётчиком вместоfor i in range(size)— снова из-за запрета встроенных функций.
print_array(arr) — вывод по индексу¶
Задание требует обращаться к элементам по индексу, а не перебором значений.
def print_array(arr: list[int]) -> None:
i = 0
while i < array_length(arr):
print(f"[{i}] = {arr[i]}") # arr[i] — доступ по индексу
i += 1
Вывод:
ranges — самописный range¶
CustomRange — это ленивый аналог встроенной range, а не просто функция, возвращающая список. Он хранит только start/stop/step и предвычисленную длину, а каждый элемент вычисляет арифметически по запросу. custom_range — строчный псевдоним этого же класса (custom_range = CustomRange), чтобы вызовы выглядели как у встроенной range.
Как устроена настоящая range в CPython¶
Чтобы понять, что именно мы воспроизводим, полезно знать внутреннее устройство оригинала. range — это C-объект rangeobject (Objects/rangeobject.c):
typedef struct {
PyObject_HEAD
PyObject *start;
PyObject *stop;
PyObject *step;
PyObject *length; // предвычисленная длина
} rangeobject;
Поля хранятся как PyObject* (а не как long), поэтому range(10**100) работает — границы лежат в виде Python-int неограниченной точности. Ключевые механики:
- Длина считается один раз при создании по формуле
(stop - start + step - 1) // step(с зеркальным вариантом для отрицательного шага) и кешируется. Поэтомуlen(r)— чтение поля, O(1). - Индексация
r[i]— чистая арифметикаstart + i * stepс нормализацией отрицательного индекса через длину. Никакого перебора, O(1). - Объект сам ничего не итерирует — для прохода создаётся отдельный итератор, наращивающий значение на шаг. Список в памяти не строится.
x in rдля целогоxпроверяется арифметически: лежит лиxв границах и делится ли(x - start)нацело наstep→ O(1). Для нецелогоx(float, строка, объект) CPython откатывается к линейному перебору со сравнением==→ O(n).- Срез
r[a:b:c]строит новыйrangeс пересчитанными границами — данные не копируются. - Равенство сравнивает диапазоны по порождаемой последовательности, а не по сырым параметрам:
range(0, 3, 2) == range(0, 4, 2), потому что оба дают[0, 2]. Хеш согласован с этим.
CustomRange — наша реализация¶
CustomRange повторяет все эти свойства. Он наследуется от collections.abc.Sequence — благодаря этому становится «настоящей» последовательностью и работает как drop-in там, где нужен sequence-протокол (например, random.sample(custom_range(1, 100), 7) в лабах). Встроенные range/max внутри намеренно не используются.
Конструктор и длина¶
class CustomRange(Sequence):
def __init__(self, start, stop=None, step=1):
if stop is None: # форма CustomRange(stop)
start, stop = 0, start
if step == 0:
raise ValueError("CustomRange() arg 3 must not be zero")
self.start, self.stop, self.step = start, stop, step
if step > 0:
raw = (stop - start + step - 1) // step
else:
raw = (start - stop - step - 1) // (-step)
self._length = raw if raw > 0 else 0 # без встроенного max
stop is None→ единственный аргумент трактуется как верхняя граница, старт сдвигается на 0 — как уrange.step == 0запрещён (иначе бесконечный цикл) →ValueError.- Длина вычисляется один раз той же формулой, что в CPython, и кешируется в
_length.len()потом — O(1).
Индексация и срез — O(1)¶
def __getitem__(self, index):
if isinstance(index, slice): # срез → новый CustomRange
start, stop, step = index.indices(self._length)
return CustomRange(
self.start + start * self.step,
self.start + stop * self.step,
self.step * step,
)
if index < 0:
index += self._length # нормализация отрицательного
if not 0 <= index < self._length:
raise IndexError("CustomRange object index out of range")
return self.start + index * self.step # i-й элемент — арифметика
r[i] — это start + i * step, без перебора. Срез не копирует элементы, а строит новый CustomRange с пересчитанными границами.
Ленивая итерация¶
def __iter__(self):
value = self.start
count = 0
while count < self._length:
yield value
value += self.step
count += 1
Значение наращивается на шаг в генераторе — список в памяти не материализуется. Поэтому custom_range(10**18) создаётся мгновенно. __reversed__ устроен симметрично (идём от последнего элемента вниз).
Проверка вхождения — O(1) для целых¶
def __contains__(self, x):
if not isinstance(x, int):
return any(v == x for v in self) # медленный путь, O(n)
if self._length == 0:
return False
if self.step > 0:
in_bounds = self.start <= x < self.stop
else:
in_bounds = self.stop < x <= self.start
return in_bounds and (x - self.start) % self.step == 0
Тот же приём, что в CPython: для целого — проверка границ и делимости за O(1); для нецелого — откат к перебору. index() и count() опираются на ту же арифметику.
Равенство и хеш — по последовательности¶
def __eq__(self, other):
if not isinstance(other, CustomRange):
return NotImplemented
if self._length != other._length:
return False
if self._length == 0:
return True
if self.start != other.start:
return False
if self._length == 1:
return True
return self.step == other.step
Два диапазона равны, если порождают одну и ту же последовательность, даже при разных параметрах: CustomRange(0, 3, 2) == CustomRange(0, 4, 2) (оба дают [0, 2]). __hash__ обнуляет незначимые поля (для пустого и одноэлементного диапазона step/start не влияют), чтобы равные объекты имели равный хеш — иначе они вели бы себя некорректно в set/dict.
Свойства и отличия от оригинала¶
| Операция | CustomRange |
Сложность |
|---|---|---|
len(r) |
чтение _length |
O(1) |
r[i] |
start + i * step |
O(1) |
x in r (целое) |
арифметика | O(1) |
x in r (нецелое) |
перебор | O(n) |
| итерация | генератор, без списка | O(n), O(1) память |
r[a:b:c] |
новый CustomRange |
O(1) |
Главное упрощение против CPython: всё на Python-int (что бесплатно даёт неограниченную точность), нет разделения на быстрый/общий итераторы и нет поддержки __index__ для объектов, маскирующихся под int.
Используется в lab02 и lab02_random при построении «холста» для визуализации дерева (for _ in custom_range(rows)) и как популяция для random.sample.
Где это в проде¶
Сами по себе array_length и print_array — учебные заглушки встроенных функций, но CustomRange иллюстрирует приём, без которого не обходится ни один эксплуатационный скрипт, — ленивые вычисления:
- Ленивая итерация = константная память на любом объёме.
CustomRangeне материализует список, а вычисляет элементы на ходу — поэтомуcustom_range(10**18)создаётся мгновенно. Тот же принцип у генераторов Python и итераторов/каналов Go: читать гигабайтный лог построчно (lab10), а не грузить его в память целиком; стримить ответ HTTP, а не собирать в буфер. Разница междуrangeи list — ровно об этом. - Sequence-протокол. Наследование от
collections.abc.Sequenceдаёт объекту индексацию, срезы иin«бесплатно» — пример того, как стандартные интерфейсы (в духеio.Readerв Go) позволяют своим типам работать с чужим кодом без переделок.