В Python квадратные скобки — один из самых универсальных синтаксических элементов. Вы используете их для доступа к элементам списка по индексу, для получения значения из словаря по ключу, для получения символа строки, для среза. При этом синтаксис всегда одинаков: obj[key].
lst = [10, 20, 30]
print(lst[1]) # 20 — доступ по индексу
dct = {"name": "Alice"}
print(dct["name"]) # Alice — доступ по ключу
s = "hello"
print(s[0]) # h — доступ к символу
print(lst[1:3]) # [20, 30] — срезЗа всем этим стоит единый механизм. Когда Python видит obj[key], он вызывает obj.__getitem__(key). Когда видит obj[key] = value — вызывает obj.__setitem__(key, value). Когда видит del obj[key] — вызывает obj.__delitem__(key).
Реализовав эти три метода в собственном классе, вы получаете объект, который работает со скобочной нотацией точно так же, как встроенные типы. Причём «ключ» в этой нотации может быть чем угодно: целым числом, строкой, объектом slice, кортежем — всё зависит от того, как вы реализуете эти методы.
Совокупность __getitem__, __setitem__ и __delitem__ называется протоколом последовательности (или протоколом контейнера). Это одна из причин, почему Python называют языком с «утиной типизацией»: объект не обязан наследоваться от list или dict, чтобы вести себя как список или словарь — достаточно реализовать нужные методы.
__getitem__ вызывается при любом обращении к объекту через квадратные скобки для чтения. Метод принимает один аргумент — ключ — и должен вернуть соответствующее значение или выбросить исключение, если ключ не найден.
Начнём с минимального примера, чтобы увидеть чистую механику:
class NumberSquares:
"""
Объект, который для любого целого числа n возвращает n².
Данные не хранятся — значение вычисляется при каждом обращении.
"""
def __getitem__(self, n):
# n — это то, что передано внутри квадратных скобок
if not isinstance(n, int):
raise TypeError(f"Индекс должен быть целым числом, получен {type(n).__name__}")
if n < 0:
raise IndexError("Отрицательные индексы не поддерживаются")
return n ** 2
squares = NumberSquares()
print(squares[5]) # 25
print(squares[10]) # 100
print(squares[0]) # 0
try:
print(squares["five"])
except TypeError as e:
print(e) # Индекс должен быть целым числом, получен strВажно: Python не преобразует ключ автоматически. Если вы написали squares[-1], в __getitem__ придёт именно -1 — без каких-либо корректировок. Обработка отрицательных индексов (как в списках, где -1 означает последний элемент) — полностью ваша ответственность.
Для корректной обработки отрицательных индексов в контейнерах с известным размером используется стандартный приём:
def __getitem__(self, index):
# Преобразуем отрицательный индекс в положительный
if index < 0:
index = len(self._data) + index
if not 0 <= index < len(self._data):
raise IndexError(f"Индекс {index} выходит за пределы допустимого диапазона")
return self._data[index]Исключение при отсутствии ключа: для индексируемых по числу объектов (аналог списка) принято выбрасывать IndexError. Для объектов с произвольными ключами (аналог словаря) — KeyError. Это не синтаксическое требование Python, но следование этому соглашению делает ваш объект предсказуемым для тех, кто будет его использовать.
__setitem__ вызывается при операции присваивания через квадратные скобки: obj[key] = value. Метод принимает два аргумента: ключ и значение, и ничего не возвращает.
class LoggedDict:
"""
Словарь, который логирует каждую операцию записи.
"""
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
# Логируем операцию перед сохранением
print(f"[SET] {key!r} = {value!r}")
self._data[key] = value
def __repr__(self):
return f"LoggedDict({self._data!r})"
d = LoggedDict()
d["name"] = "Alice" # [SET] 'name' = 'Alice'
d["age"] = 28 # [SET] 'age' = 28
d["name"] = "Bob" # [SET] 'name' = 'Bob' — перезапись
print(d["name"]) # BobПрактически важный случай — валидация типов при записи. В веб-разработке это используется при реализации схем данных: объект принимает данные от клиента и немедленно проверяет их корректность:
class StrictDict:
"""
Словарь с проверкой типов при записи.
Схема передаётся при создании: {ключ: ожидаемый_тип}.
"""
def __init__(self, schema):
self._schema = schema # {'name': str, 'age': int, 'is_active': bool}
self._data = {}
def __getitem__(self, key):
if key not in self._data:
raise KeyError(f"Ключ {key!r} не найден")
return self._data[key]
def __setitem__(self, key, value):
if key not in self._schema:
raise KeyError(f"Ключ {key!r} не определён в схеме")
expected_type = self._schema[key]
if not isinstance(value, expected_type):
raise TypeError(
f"Поле {key!r} ожидает {expected_type.__name__}, "
f"получен {type(value).__name__}"
)
self._data[key] = value
def __repr__(self):
return f"StrictDict({self._data!r})"
user_schema = StrictDict({
"username": str,
"age": int,
"is_active": bool,
})
user_schema["username"] = "alice"
user_schema["age"] = 28
user_schema["is_active"] = True
print(user_schema["username"]) # alice
try:
user_schema["age"] = "двадцать восемь"
except TypeError as e:
print(e) # Поле 'age' ожидает int, получен str
try:
user_schema["email"] = "alice@example.com"
except KeyError as e:
print(e) # Ключ 'email' не определён в схеме__delitem__ вызывается оператором del при удалении элемента по ключу: del obj[key]. Метод принимает ключ и ничего не возвращает.
class ProtectedDict:
"""
Словарь, в котором некоторые ключи защищены от удаления.
"""
def __init__(self, protected_keys=None):
self._data = {}
self._protected = set(protected_keys or [])
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
def __delitem__(self, key):
if key in self._protected:
raise KeyError(
f"Ключ {key!r} защищён от удаления"
)
if key not in self._data:
raise KeyError(f"Ключ {key!r} не найден")
del self._data[key]
def __repr__(self):
return f"ProtectedDict(data={self._data!r}, protected={self._protected!r})"
cfg = ProtectedDict(protected_keys=["SECRET_KEY", "DATABASE_URL"])
cfg["SECRET_KEY"] = "super-secret"
cfg["DEBUG"] = True
cfg["PORT"] = 8080
del cfg["PORT"] # успешно — PORT не защищён
print(cfg["DEBUG"]) # True
try:
del cfg["SECRET_KEY"]
except KeyError as e:
print(e) # Ключ 'SECRET_KEY' защищён от удаленияОдин из самых неочевидных аспектов протокола: когда вы пишете obj[1:3] или obj[::2], Python не передаёт в __getitem__ два отдельных числа. Он создаёт специальный объект типа slice и передаёт его как единственный аргумент.
class SliceDemo:
def __getitem__(self, key):
print(f"Тип ключа: {type(key).__name__}")
print(f"Значение ключа: {key!r}")
demo = SliceDemo()
demo[5] # Тип ключа: int, Значение ключа: 5
demo[1:3] # Тип ключа: slice, Значение ключа: slice(1, 3, None)
demo[::2] # Тип ключа: slice, Значение ключа: slice(None, None, 2)
demo[1:10:3] # Тип ключа: slice, Значение ключа: slice(1, 10, 3)Объект slice имеет три атрибута: start, stop и step. Любой из них может быть None, если соответствующая часть среза не указана. Для корректной обработки среза используется метод slice.indices(length), который возвращает реальные значения start, stop, step с учётом длины коллекции:
s = slice(1, 10, 2)
# Для коллекции длиной 6:
start, stop, step = s.indices(6)
print(start, stop, step) # 1 6 2
# Метод автоматически ограничивает stop длиной коллекцииРеализуем класс, который корректно обрабатывает и индексы, и срезы:
class CustomList:
"""
Список с поддержкой индексов (включая отрицательные) и срезов.
"""
def __init__(self, data):
self._data = list(data)
def __getitem__(self, key):
if isinstance(key, int):
# Обычный индекс — делегируем встроенному списку,
# который сам обработает отрицательные значения и IndexError
return self._data[key]
elif isinstance(key, slice):
# Срез — возвращаем новый объект того же типа
return CustomList(self._data[key])
else:
raise TypeError(
f"Индекс должен быть int или slice, получен {type(key).__name__}"
)
def __setitem__(self, key, value):
if isinstance(key, (int, slice)):
self._data[key] = value
else:
raise TypeError(f"Недопустимый тип индекса: {type(key).__name__}")
def __delitem__(self, key):
if isinstance(key, (int, slice)):
del self._data[key]
else:
raise TypeError(f"Недопустимый тип индекса: {type(key).__name__}")
def __len__(self):
return len(self._data)
def __repr__(self):
return f"CustomList({self._data!r})"
lst = CustomList([10, 20, 30, 40, 50])
print(lst[0]) # 10
print(lst[-1]) # 50 — обрабатывается встроенным списком
print(lst[1:3]) # CustomList([20, 30])
print(lst[::2]) # CustomList([10, 30, 50])
lst[1] = 99
print(lst) # CustomList([10, 99, 30, 40, 50])
del lst[0]
print(lst) # CustomList([99, 30, 40, 50])Обратите внимание: lst[1:3] возвращает не список, а снова CustomList. Это важное архитектурное решение: операции над объектом должны возвращать объекты того же типа, сохраняя поведение и методы. Это называется принципом закрытости под операциями.
Это один из самых неочевидных моментов темы. Если у объекта определён __getitem__, но не определён __iter__, Python способен итерироваться по нему автоматически. Интерпретатор начинает вызывать __getitem__(0), __getitem__(1), __getitem__(2) и так далее, пока не получит IndexError — это сигнал об окончании последовательности.
class LegacySequence:
"""
Класс без __iter__, но с __getitem__.
Python всё равно сможет итерироваться по нему.
"""
def __init__(self, data):
self._data = data
def __getitem__(self, index):
return self._data[index] # IndexError при выходе за границы
seq = LegacySequence([10, 20, 30])
# for работает через __getitem__, даже без __iter__
for item in seq:
print(item)
# 10
# 20
# 30
# list() тоже работает
print(list(seq)) # [10, 20, 30]
# И оператор in (через последовательный перебор)
print(20 in seq) # TrueЭтот механизм существует по историческим причинам: в ранних версиях Python __iter__ не было, и итерация реализовывалась только через __getitem__. Сегодня этот fallback сохраняется для совместимости.
Почему важно знать об этом? По двум причинам. Первая — ваш класс может стать итерируемым непреднамеренно, если вы реализовали __getitem__ для другой цели. Вторая — если ваш объект логически является последовательностью, явная реализация __iter__ предпочтительнее: она даёт контроль над поведением, эффективнее при вложенных циклах и делает намерение явным.
Оператор in (item in obj) по умолчанию работает через последовательный перебор с помощью __getitem__ или __iter__. Это означает линейную сложность O(n) — для каждой проверки Python обходит весь контейнер.
Если ваш объект хранит данные в структуре, которая позволяет проверить принадлежность за O(1) (словарь, множество), определите метод __contains__ явно:
class FastContainer:
def __init__(self, items):
self._data = list(items)
self._lookup = set(items) # для O(1) проверки
def __getitem__(self, index):
return self._data[index]
def __contains__(self, item):
# O(1) вместо O(n) — используем множество для поиска
return item in self._lookup
def __len__(self):
return len(self._data)
container = FastContainer([1, 2, 3, 4, 5])
print(3 in container) # True — использует __contains__
print(99 in container) # False — O(1), не перебирает все элементыЕсли __contains__ не определён, но есть __iter__ — Python использует его для последовательного поиска. Если нет ни __contains__, ни __iter__, но есть __getitem__ — Python использует fallback через __getitem__.
В HTTP каждый запрос и ответ несут набор заголовков — пар «имя: значение». Имена заголовков по стандарту нечувствительны к регистру: Content-Type, content-type и CONTENT-TYPE — это один и тот же заголовок.
В Django и DRF объект запроса предоставляет заголовки именно с таким поведением. Реализуем его:
class Headers:
"""
Коллекция HTTP-заголовков с нечувствительным к регистру доступом.
Имена заголовков нормализуются к нижнему регистру при хранении.
Пример:
h["Content-Type"] и h["content-type"] — одно и то же.
"""
def __init__(self, initial=None):
# Храним заголовки в нижнем регистре
self._headers = {}
if initial:
for key, value in initial.items():
self[key] = value # используем __setitem__ для нормализации
def _normalize(self, key):
"""Приводим имя заголовка к нижнему регистру."""
if not isinstance(key, str):
raise TypeError(f"Имя заголовка должно быть строкой, получен {type(key).__name__}")
return key.lower()
def __getitem__(self, key):
normalized = self._normalize(key)
if normalized not in self._headers:
raise KeyError(f"Заголовок {key!r} не найден")
return self._headers[normalized]
def __setitem__(self, key, value):
normalized = self._normalize(key)
self._headers[normalized] = str(value) # значение всегда строка
def __delitem__(self, key):
normalized = self._normalize(key)
if normalized not in self._headers:
raise KeyError(f"Заголовок {key!r} не найден")
del self._headers[normalized]
def __contains__(self, key):
return self._normalize(key) in self._headers
def __iter__(self):
return iter(self._headers)
def __len__(self):
return len(self._headers)
def get(self, key, default=None):
"""Безопасное получение заголовка без исключения."""
try:
return self[key]
except KeyError:
return default
def items(self):
return self._headers.items()
def __repr__(self):
return f"Headers({self._headers!r})"Проверим работу:
headers = Headers({
"Content-Type": "application/json",
"Authorization": "Bearer abc123",
"X-Request-Id": "req-001",
})
# Нечувствительность к регистру при чтении
print(headers["Content-Type"]) # application/json
print(headers["content-type"]) # application/json
print(headers["CONTENT-TYPE"]) # application/json
# Проверка наличия тоже нечувствительна к регистру
print("authorization" in headers) # True
print("Authorization" in headers) # True
print("X-Api-Key" in headers) # False
# Запись
headers["Cache-Control"] = "no-cache"
print(headers["cache-control"]) # no-cache
# Безопасное получение
print(headers.get("Accept", "application/json")) # application/json (default)
# Удаление
del headers["X-Request-Id"]
print("x-request-id" in headers) # False
# Итерация (по нормализованным именам)
for name in headers:
print(f"{name}: {headers[name]}")
# content-type: application/json
# authorization: Bearer abc123
# cache-control: no-cache
print(repr(headers))Этот класс демонстрирует важный архитектурный принцип: нормализация данных при записи (а не при чтении) гарантирует, что внутри объекта всегда хранится единообразное представление, независимо от того, как данные были переданы снаружи.
Конфигурация веб-приложения — ещё один классический случай для протокола контейнера. Реализуем объект конфигурации, который поддерживает доступ по ключу, вложенные пути через точку и защиту обязательных ключей:
class Config:
"""
Конфигурация веб-приложения с доступом по ключу.
Особенности:
- Поддержка вложенных путей: config["database.host"]
- Защита обязательных ключей от удаления
- Типизированные значения через схему
Пример:
config["database.host"] эквивалентно config["database"]["host"]
"""
def __init__(self, data=None, required_keys=None):
self._data = data or {}
self._required = set(required_keys or [])
def __getitem__(self, key):
# Поддерживаем вложенные пути через точку
if "." in key:
parts = key.split(".", 1) # разбиваем только по первой точке
top, rest = parts[0], parts[1]
if top not in self._data:
raise KeyError(f"Ключ {top!r} не найден в конфигурации")
nested = self._data[top]
if isinstance(nested, dict):
# Рекурсивно создаём Config для вложенного словаря
return Config(nested)[rest]
raise KeyError(f"Значение по ключу {top!r} не является вложенной конфигурацией")
if key not in self._data:
raise KeyError(f"Параметр {key!r} не найден в конфигурации")
return self._data[key]
def __setitem__(self, key, value):
# Вложенные пути при записи — тоже поддерживаем
if "." in key:
parts = key.split(".", 1)
top, rest = parts[0], parts[1]
if top not in self._data:
self._data[top] = {}
if isinstance(self._data[top], dict):
Config(self._data[top])[rest] = value
return
self._data[key] = value
def __delitem__(self, key):
if key in self._required:
raise KeyError(
f"Параметр {key!r} является обязательным и не может быть удалён"
)
if key not in self._data:
raise KeyError(f"Параметр {key!r} не найден")
del self._data[key]
def __contains__(self, key):
if "." in key:
try:
self[key]
return True
except KeyError:
return False
return key in self._data
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def __repr__(self):
return f"Config({self._data!r})"
# Создаём конфигурацию приложения
config = Config(
data={
"DEBUG": False,
"SECRET_KEY": "a-very-long-secret-key",
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp_db",
},
"redis": {
"host": "localhost",
"port": 6379,
}
},
required_keys=["SECRET_KEY", "database"]
)
# Доступ к простым ключам
print(config["DEBUG"]) # False
print(config["SECRET_KEY"]) # a-very-long-secret-key
# Доступ через вложенные пути
print(config["database.host"]) # localhost
print(config["database.port"]) # 5432
print(config["redis.host"]) # localhost
# Проверка наличия
print("DEBUG" in config) # True
print("database.host" in config) # True
print("database.password" in config) # False
# Запись вложенного значения
config["database.password"] = "secret"
print(config["database.password"]) # secret
# Безопасное получение с дефолтом
print(config.get("EMAIL_HOST", "smtp.gmail.com")) # smtp.gmail.com
# Удаление обычного ключа
config["DEBUG"] = True
del config["DEBUG"]
print("DEBUG" in config) # False
# Попытка удалить обязательный ключ
try:
del config["SECRET_KEY"]
except KeyError as e:
print(e) # Параметр 'SECRET_KEY' является обязательным и не может быть удалёнОбъединим все три метода в одном классе, демонстрирующем их совместную работу. TypedList — это список с проверкой типов при добавлении элементов, поддержкой срезов и защитой от некорректных операций:
class TypedList:
"""
Список с ограничением типов хранимых элементов.
Поддерживает индексный доступ (включая отрицательные индексы),
срезы, запись и удаление.
"""
def __init__(self, item_type, initial=None):
self._type = item_type # тип элементов, который разрешён
self._data = []
if initial:
for item in initial:
self._validate(item)
self._data.append(item)
def _validate(self, value):
"""Проверяем, что значение соответствует разрешённому типу."""
if not isinstance(value, self._type):
raise TypeError(
f"TypedList[{self._type.__name__}] не принимает "
f"{type(value).__name__}: {value!r}"
)
def __getitem__(self, key):
if isinstance(key, (int, slice)):
result = self._data[key]
# При срезе возвращаем TypedList, а не список
if isinstance(key, slice):
return TypedList(self._type, result)
return result
raise TypeError(f"Недопустимый тип индекса: {type(key).__name__}")
def __setitem__(self, key, value):
if isinstance(key, slice):
# При срезовой записи проверяем каждый элемент
for item in value:
self._validate(item)
self._data[key] = value
elif isinstance(key, int):
self._validate(value)
self._data[key] = value
else:
raise TypeError(f"Недопустимый тип индекса: {type(key).__name__}")
def __delitem__(self, key):
if isinstance(key, (int, slice)):
del self._data[key]
else:
raise TypeError(f"Недопустимый тип индекса: {type(key).__name__}")
def append(self, value):
self._validate(value)
self._data.append(value)
def __contains__(self, item):
return item in self._data
def __iter__(self):
return iter(self._data)
def __len__(self):
return len(self._data)
def __repr__(self):
return f"TypedList[{self._type.__name__}]({self._data!r})"Демонстрация работы:
# Список только из строк
string_list = TypedList(str, ["alice", "bob", "carol"])
print(string_list[0]) # alice
print(string_list[-1]) # carol
print(string_list[0:2]) # TypedList[str](['alice', 'bob'])
string_list[1] = "dave"
print(string_list) # TypedList[str](['alice', 'dave', 'carol'])
# Типизированная проверка при записи
try:
string_list[0] = 42
except TypeError as e:
print(e) # TypedList[str] не принимает int: 42
# Добавление корректного и некорректного элемента
string_list.append("eve")
try:
string_list.append(3.14)
except TypeError as e:
print(e) # TypedList[str] не принимает float: 3.14
# Удаление
del string_list[0]
print(string_list) # TypedList[str](['dave', 'carol', 'eve'])
# Срез возвращает TypedList, а не список
subset = string_list[0:2]
print(type(subset).__name__) # TypedList
print(subset) # TypedList[str](['dave', 'carol'])
# Оператор in
print("carol" in string_list) # True
print("alice" in string_list) # FalseОтрицательные индексы не обрабатываются автоматически. Если вы реализуете __getitem__ самостоятельно (без делегирования встроенному списку), вы обязаны обработать отрицательные индексы сами. Python не преобразует -1 в len - 1 за вас:
class BadSequence:
def __init__(self):
self._data = [10, 20, 30]
def __getitem__(self, index):
# Не обрабатываем отрицательные индексы
if index >= len(self._data):
raise IndexError("out of range")
return self._data[index] # -1 попадёт прямо сюда и вернёт _data[-1] случайно
seq = BadSequence()
print(seq[-1]) # 30 — работает случайно, потому что list сам понимает -1
# Но если хранить данные иначе — поведение будет непредсказуемымПравильный подход при ручной реализации:
def __getitem__(self, index):
if isinstance(index, int):
if index < 0:
index += len(self._data) # преобразуем отрицательный
if not 0 <= index < len(self._data):
raise IndexError(f"index {index} out of range")
return self._data[index]KeyError vs IndexError — не перепутайте. Соглашение Python: контейнеры с целочисленными индексами (как список) при выходе за границы выбрасывают IndexError. Контейнеры с произвольными ключами (как словарь) при отсутствующем ключе выбрасывают KeyError. Нарушение этого соглашения сломает совместимость с кодом, который ожидает стандартного поведения:
# Если ваш объект похож на список — используйте IndexError
def __getitem__(self, index):
if index >= len(self._data):
raise IndexError(f"index {index} out of range") # правильно
# Если ваш объект похож на словарь — используйте KeyError
def __getitem__(self, key):
if key not in self._data:
raise KeyError(key) # правильноВозврат None вместо исключения нарушает контракт. Иногда возникает соблазн вернуть None при отсутствующем ключе — это «мягче» и не прерывает работу программы. Но это скрывает ошибки: код, ожидающий реальное значение, получит None и сломается позже, в совершенно другом месте. Если хотите «мягкое» поведение — добавьте отдельный метод get(key, default=None), как это сделано в dict.
__setitem__ не вызывается при инициализации объекта. Если в __init__ вы пишете self._data = {} — это обычное присваивание атрибута, не вызов __setitem__. __setitem__ вызывается только при obj[key] = value после создания объекта.
Три метода — __getitem__, __setitem__, __delitem__ — реализуют протокол контейнера: они стоят за синтаксисом квадратных скобок в Python. Реализовав их, объект получает возможность поддерживать индексный доступ, запись и удаление элементов — аналогично встроенным спискам и словарям.
Ключ в __getitem__ может быть не только целым числом, но и строкой, объектом slice или любым другим объектом. При срезах obj[1:3] Python автоматически создаёт объект slice и передаёт его методу. Для правильной обработки используется slice.indices(length).
Если __iter__ не определён, но определён __getitem__ — Python автоматически использует его для итерации, вызывая __getitem__(0), __getitem__(1) и т.д. до IndexError. Это legacy-механизм; явная реализация __iter__ предпочтительнее.
Оператор in работает через __contains__ (если определён), иначе — через __iter__ или __getitem__. Для контейнеров с быстрым поиском (на основе словаря или множества) явная реализация __contains__ критически важна для производительности.
- Какие методы вызывает Python при выполнении операций
obj[key],obj[key] = valueиdel obj[key]? Есть ли у этих методов возвращаемое значение? - Что именно Python передаёт в
__getitem__при выполненииobj[1:5:2]? Как получить конкретные значения из этого аргумента? - Какое исключение принято выбрасывать в
__getitem__при отсутствии ключа:KeyErrorилиIndexError? От чего это зависит? - Что произойдёт, если в классе определён
__getitem__, но не определён__iter__? Будет ли работать циклfor item in obj:? - Как работает оператор
inдля объекта, у которого определены__getitem__и__contains__? Какой метод будет использован? - Обрабатывает ли Python отрицательные индексы автоматически перед передачей в
__getitem__? Что произойдёт, если не обработать-1в собственной реализации? - В примере с классом
Headersимена заголовков нормализуются при записи, а не при чтении. Почему это лучше, чем нормализация при чтении? - Почему возврат
Noneвместо исключения при отсутствующем ключе в__getitem__считается плохой практикой? Какой правильный способ реализовать «мягкое» поведение?
Класс SquaresMap
Создайте класс SquaresMap, который реализует только __getitem__ и ведёт себя как отображение: obj[n] возвращает n² для любого целого числа n.
Отрицательные числа разрешены.
Для нецелых ключей выбрасывайте TypeError с сообщением "Ключ должен быть целым числом".
Реализуйте __contains__, который возвращает True для любого целого числа (поскольку квадрат определён для любого целого).
Реализуйте __repr__ в формате SquaresMap().
Пример использования:
sm = SquaresMap()
print(sm[5]) # 25
print(sm[-3]) # 9
print(sm[0]) # 0
print(sm[10]) # 100
print(5 in sm) # True
print(0 in sm) # True
try:
sm["five"]
except TypeError as e:
print(e) # Ключ должен быть целым числом
# Использование в цикле через явные вызовы
for i in range(5):
print(f"{i}² = {sm[i]}")Класс RingBuffer
Создайте класс RingBuffer — кольцевой буфер фиксированного размера. Принимает один аргумент: capacity (ёмкость буфера, целое число).
Реализуйте метод push(value), который добавляет элемент: если буфер заполнен, самый старый элемент перезаписывается.
Реализуйте __getitem__, поддерживающий индексный доступ (включая отрицательные индексы) и срезы.
При выходе за границы выбрасывайте IndexError.
Реализуйте __len__, возвращающий текущее количество элементов (не ёмкость).
Реализуйте __repr__ в формате RingBuffer(capacity=5, data=[...]).
Пример использования:
buf = RingBuffer(capacity=4)
buf.push(10)
buf.push(20)
buf.push(30)
buf.push(40)
print(len(buf)) # 4
print(buf[0]) # 10
print(buf[-1]) # 40
print(buf[1:3]) # [20, 30]
# Буфер заполнен — следующий push перезаписывает старейший элемент
buf.push(50)
print(len(buf)) # 4 — ёмкость не изменилась
print(buf[0]) # 20 — 10 перезаписан
print(buf[-1]) # 50 — новый элемент
print(list(buf[::1])) # [20, 30, 40, 50]Класс HttpHeaders
Создайте класс HttpHeaders — улучшенный вариант класса Headers из лекции. Принимает необязательный словарь initial при создании.
При инициализации должны создаваться следующие атрибуты объекта класса:
self._data = {}- должен будет хранить пары:ключ в нижнем регистре→значениеself._original = {}- должен будет хранить пары:ключ в нижнем регистре→оригинальное имя- Если передан словарь
initial, то в объекте должны создаваться атрибуты из пары переданного словаря.
Реализуйте:
__setitem__записать в_data:ключ в нижнем регистре→значение; в_original:ключ в нижнем регистре→оригинальное имя. Запись производить с проверкой: имя заголовка должно быть непустой строкой, значение — строкой или числом (при записи числа конвертируются в строки).__getitem__возвращать значения из_dataпо ключу с нечувствительностью к регистру.__delitem__удаляет пары из_dataи_original. Должен выбрасыватьKeyErrorпри попытке удалить несуществующий заголовок.
Реализуйте:
__contains__(нечувствительно к регистру),__iter__(возвращает итератор с оригинальными именами в порядке добавления),__len__.
Добавьте метод get(key, default=None).
Реализуйте __repr__ в формате HttpHeaders({'content-type': 'application/json', ...}).
Пример использования:
h = HttpHeaders({
"Content-Type": "application/json",
"X-Request-Id": "abc-123",
})
print(h["content-type"]) # application/json
print(h["CONTENT-TYPE"]) # application/json
print(h["X-Request-Id"]) # abc-123
h["Cache-Control"] = "no-cache"
h["X-Retry-Count"] = 3 # число конвертируется в строку
print(h["x-retry-count"]) # 3
print("content-type" in h) # True
print("Authorization" in h) # False
print(len(h)) # 4
for name in h:
print(f"{name}: {h[name]}")
del h["X-Request-Id"]
print("x-request-id" in h) # False
print(h.get("Authorization", "Bearer default")) # Bearer defaultКласс QueryParams
Создайте класс QueryParams — объект для работы с параметрами URL-запроса вида ?page=1&limit=20&sort=name.
Объект можно создать двумя способами:
params = QueryParams("page=1&limit=20&sort=name") # из строки
params = QueryParams({"page": "1", "limit": "20"}) # из словаря
params = QueryParams() # пустой объектПри инициализации должен создаваться следующий атрибут:
self._data = {}— хранит парыключ (str) → значение (str). Все значения хранятся как строки.
Если передана строка — разбейте её по символу &, затем каждую часть по символу = (только по первому вхождению). Пары ключ=значение сохраните в _data. Если передан словарь — скопируйте его в _data, приведя все значения к строке.
Реализуйте:
__getitem__— возвращает значение из_dataпо ключу. Если ключ не найден — выбрасываетKeyError.__setitem__— записывает пару в_data. Ключ и значение приводятся к строке (str(key),str(value)).__delitem__— удаляет пару из_data. Если ключ не найден — выбрасываетKeyError.__contains__— возвращаетTrue, если ключ присутствует в_data.__len__— возвращает количество параметров в_data.
Добавьте методы:
get(key, default=None)— возвращает значение по ключу илиdefault, если ключ не найден (без выброса исключения).to_string()— возвращает строку в формате URL-параметров. Параметры должны быть отсортированы по имени ключа и разделены символом&. Формат:"filter=active&limit=20&page=2".
Реализуйте __repr__ в формате QueryParams(page=2, limit=20, sort=name) — перечисление пар ключ=значение через запятую.
Пример использования:
params = QueryParams("page=1&limit=20&sort=name&order=asc")
print(params["page"]) # 1
print(params["limit"]) # 20
print(params["sort"]) # name
print("page" in params) # True
print("token" in params) # False
print(len(params)) # 4
params["page"] = 2
params["filter"] = "active"
del params["order"]
print(params.to_string())
# filter=active&limit=20&page=2&sort=name
print(params.get("offset", "0")) # 0
print(repr(params))
# QueryParams(filter=active, limit=20, page=2, sort=name)Класс Matrix
Создайте класс Matrix — двумерная матрица целых чисел.
Объект создаётся следующей командой:
m = Matrix(rows, cols)где rows — количество строк, cols — количество столбцов (оба аргумента — положительные целые числа).
При инициализации должен создаваться следующий атрибут:
self._data— список изrowsстрок, каждая строка — список изcolsнулей.
Доступ к элементам матрицы осуществляется через кортеж (row, col):
m[0, 0] = 5 # запись
print(m[1, 2]) # чтениеРеализуйте:
__getitem__— принимает ключ-кортеж(row, col). Извлекает и возвращает элементself._data[row][col]. Перед обращением к данным вызывайте вспомогательный метод проверки индексов.__setitem__— принимает ключ-кортеж(row, col)и значение. Записывает значение вself._data[row][col]. Перед записью вызывайте вспомогательный метод проверки индексов.__delitem__— удаление элемента матрицы не имеет смысла. При любом вызовеdel m[row, col]выбрасыватьTypeError("Удаление элементов матрицы не поддерживается").
Реализуйте вспомогательный метод _check_index(self, row, col), который проверяет, что row находится в диапазоне [0, rows), а col — в диапазоне [0, cols). Если любой из индексов некорректен — выбрасывать IndexError с сообщением, указывающим какой именно индекс вышел за пределы и каков допустимый размер.
Добавьте методы:
row(n)— возвращает список элементов строки с номеромn(копию, не ссылку).col(n)— возвращает список элементов столбца с номеромn.
Реализуйте __repr__ в следующем многострочном формате:
Matrix 3x3:
[1, 0, 3]
[0, 5, 0]
[0, 0, 9]
Пример использования:
m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 5
m[2, 2] = 9
m[0, 2] = 3
print(m[1, 1]) # 5
print(m[0, 2]) # 3
print(m.row(0)) # [1, 0, 3]
print(m.col(2)) # [3, 0, 9]
try:
m[5, 0] = 10
except IndexError as e:
print(e) # Индекс строки 5 выходит за пределы (размер: 3)
try:
del m[0, 0]
except TypeError as e:
print(e) # Удаление элементов матрицы не поддерживается
print(m)
# Matrix 3x3:
# [1, 0, 3]
# [0, 5, 0]
# [0, 0, 9]Класс JSONPath
Создайте класс JSONPath — обёртка над вложенным словарём Python, которая позволяет читать, записывать и удалять глубоко вложенные значения через путь с точкой.
Объект создаётся следующей командой:
data = JSONPath({"user": {"name": "Alice", "address": {"city": "Moscow"}}})
data = JSONPath() # пустой объектПри инициализации должен создаваться следующий атрибут:
self._data— обычный словарь Python, переданный в аргументе (или пустой словарь, если аргумент не передан).
Путь к вложенному значению записывается через точку: "user.address.city" означает self._data["user"]["address"]["city"]. Если точки в строке нет — это обычный ключ верхнего уровня: "api_version" означает self._data["api_version"].
Реализуйте три вспомогательных метода для работы с self._data по списку ключей:
_get_nested(data, keys)— рекурсивно обходит словарь по списку ключей и возвращает значение. Если любой ключ из пути не найден — выбрасываетKeyError._set_nested(data, keys, value)— рекурсивно устанавливает значение. Если промежуточного словаря не существует — создаёт его автоматически._del_nested(data, keys)— рекурсивно удаляет ключ. Если любой ключ из пути не найден — выбрасываетKeyError.
Реализуйте магические методы, которые разбивают путь на список ключей и вызывают соответствующий вспомогательный метод:
__getitem__(path)— разбиваетpathпо точке, вызывает_get_nested. ПриKeyErrorвнутри — перевыбрасываетKeyError(path)с полным путём.__setitem__(path, value)— разбиваетpathпо точке, вызывает_set_nested.__delitem__(path)— разбиваетpathпо точке, вызывает_del_nested. ПриKeyErrorвнутри — перевыбрасываетKeyError(path)с полным путём.__contains__(path)— пытается вызватьself[path]; если успешно — возвращаетTrue, еслиKeyError— возвращаетFalse.
Реализуйте __repr__ в формате JSONPath({...}), где {...} — стандартное строковое представление self._data.
Пример использования:
data = JSONPath({
"user": {
"name": "Alice",
"address": {
"city": "Moscow",
"zip": "101000"
}
},
"api_version": "2.0"
})
print(data["api_version"]) # 2.0
print(data["user.name"]) # Alice
print(data["user.address.city"]) # Moscow
data["user.age"] = 28
print(data["user.age"]) # 28
# Если промежуточного ключа нет — он создаётся автоматически
data["server.host"] = "localhost"
data["server.port"] = 8080
print(data["server.host"]) # localhost
print("user.name" in data) # True
print("user.phone" in data) # False
del data["user.address.zip"]
print("user.address.zip" in data) # False
try:
_ = data["user.address.country"]
except KeyError as e:
print(e) # 'user.address.country'